diff --git a/gaseous-server/.DS_Store b/gaseous-server/.DS_Store index af3f72a..e33e0ab 100644 Binary files a/gaseous-server/.DS_Store and b/gaseous-server/.DS_Store differ diff --git a/gaseous-server/Assets/.DS_Store b/gaseous-server/Assets/.DS_Store index 95b3a7b..dd4ab63 100644 Binary files a/gaseous-server/Assets/.DS_Store and b/gaseous-server/Assets/.DS_Store differ diff --git a/gaseous-server/Assets/Ratings/.DS_Store b/gaseous-server/Assets/Ratings/.DS_Store index 47d5f24..50a7adc 100644 Binary files a/gaseous-server/Assets/Ratings/.DS_Store and b/gaseous-server/Assets/Ratings/.DS_Store differ diff --git a/gaseous-server/Classes/Auth/Classes/IdentityRole.cs b/gaseous-server/Classes/Auth/Classes/IdentityRole.cs new file mode 100644 index 0000000..64a2548 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/IdentityRole.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Data; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; + +namespace Authentication +{ + /// + /// Class that implements the ASP.NET Identity + /// IRole interface + /// + public class ApplicationRole : IdentityRole + { + } +} diff --git a/gaseous-server/Classes/Auth/Classes/IdentityUser.cs b/gaseous-server/Classes/Auth/Classes/IdentityUser.cs new file mode 100644 index 0000000..e263b70 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/IdentityUser.cs @@ -0,0 +1,15 @@ +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; +using System; + +namespace Authentication +{ + /// + /// Class that implements the ASP.NET Identity + /// IUser interface + /// + public class ApplicationUser : IdentityUser + { + public SecurityProfileViewModel SecurityProfile { get; set; } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/RoleStore.cs b/gaseous-server/Classes/Auth/Classes/RoleStore.cs new file mode 100644 index 0000000..3a80cb2 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/RoleStore.cs @@ -0,0 +1,171 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; +using MySqlConnector; + +namespace Authentication +{ + /// + /// Class that implements the key ASP.NET Identity role store iterfaces + /// + public class RoleStore : IQueryableRoleStore + { + private RoleTable roleTable; + public Database Database { get; private set; } + + public IQueryable Roles + { + get + { + List roles = roleTable.GetRoles(); + return roles.AsQueryable(); + } + } + + public RoleStore() + { + Database = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + roleTable = new RoleTable(Database); + } + + /// + /// Constructor that takes a MySQLDatabase as argument + /// + /// + public RoleStore(Database database) + { + Database = database; + roleTable = new RoleTable(database); + } + + public Task CreateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role == null) + { + throw new ArgumentNullException("role"); + } + + roleTable.Insert(role); + + return Task.FromResult(IdentityResult.Success); + } + + public Task DeleteAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role == null) + { + throw new ArgumentNullException("user"); + } + + roleTable.Delete(role.Id); + + return Task.FromResult(IdentityResult.Success); + } + + public Task FindByIdAsync(string roleId, CancellationToken cancellationToken) + { + ApplicationRole result = roleTable.GetRoleById(roleId) as ApplicationRole; + + return Task.FromResult(result); + } + + public Task RoleExistsAsync(string roleId, CancellationToken cancellationToken) + { + ApplicationRole? result = roleTable.GetRoleById(roleId) as ApplicationRole; + + if (result == null) + { + return Task.FromResult(false); + } + else + { + return Task.FromResult(true); + } + } + + public Task FindByNameAsync(string roleName, CancellationToken cancellationToken) + { + ApplicationRole? result = roleTable.GetRoleByName(roleName) as ApplicationRole; + + return Task.FromResult(result); + } + + public Task UpdateAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role == null) + { + throw new ArgumentNullException("user"); + } + + roleTable.Update(role); + + return Task.FromResult(IdentityResult.Success); + } + + public void Dispose() + { + if (Database != null) + { + Database = null; + } + } + + public Task GetRoleIdAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role != null) + { + return Task.FromResult(roleTable.GetRoleId(role.Name)); + } + + return Task.FromResult(null); + } + + public Task GetRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role != null) + { + return Task.FromResult(roleTable.GetRoleName(role.Id)); + } + + return Task.FromResult(null); + } + + public Task SetRoleNameAsync(ApplicationRole role, string? roleName, CancellationToken cancellationToken) + { + if (role == null) + { + throw new ArgumentNullException("role"); + } + + role.Name = roleName; + roleTable.Update(role); + + return Task.FromResult(IdentityResult.Success); + } + + public Task GetNormalizedRoleNameAsync(ApplicationRole role, CancellationToken cancellationToken) + { + if (role != null) + { + return Task.FromResult(roleTable.GetRoleName(role.Id)); + } + + return Task.FromResult(null); + } + + public Task SetNormalizedRoleNameAsync(ApplicationRole role, string? normalizedName, CancellationToken cancellationToken) + { + if (role == null) + { + throw new ArgumentNullException("role"); + } + + role.Name = normalizedName; + roleTable.Update(role); + + return Task.FromResult(IdentityResult.Success); + } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/RoleTable.cs b/gaseous-server/Classes/Auth/Classes/RoleTable.cs new file mode 100644 index 0000000..b9f7d89 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/RoleTable.cs @@ -0,0 +1,168 @@ +using System; +using System.Collections.Generic; +using System.Data; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; + +namespace Authentication +{ + /// + /// Class that represents the Role table in the MySQL Database + /// + public class RoleTable + { + private Database _database; + + /// + /// Constructor that takes a MySQLDatabase instance + /// + /// + public RoleTable(Database database) + { + _database = database; + } + + /// + /// Deltes a role from the Roles table + /// + /// The role Id + /// + public int Delete(string roleId) + { + string commandText = "Delete from Roles where Id = @id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@id", roleId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Inserts a new Role in the Roles table + /// + /// The role's name + /// + public int Insert(ApplicationRole role) + { + string commandText = "Insert into Roles (Id, Name) values (@id, @name)"; + Dictionary parameters = new Dictionary(); + parameters.Add("@name", role.Name); + parameters.Add("@id", role.Id); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Returns a role name given the roleId + /// + /// The role Id + /// Role name + public string? GetRoleName(string roleId) + { + string commandText = "Select Name from Roles where Id = @id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@id", roleId); + + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Returns the role Id given a role name + /// + /// Role's name + /// Role's Id + public string? GetRoleId(string roleName) + { + string? roleId = null; + string commandText = "Select Id from Roles where Name = @name"; + Dictionary parameters = new Dictionary() { { "@name", roleName } }; + + DataTable result = _database.ExecuteCMD(commandText, parameters); + if (result.Rows.Count > 0) + { + return Convert.ToString(result.Rows[0][0]); + } + + return roleId; + } + + /// + /// Gets the ApplicationRole given the role Id + /// + /// + /// + public ApplicationRole? GetRoleById(string roleId) + { + var roleName = GetRoleName(roleId); + ApplicationRole? role = null; + + if(roleName != null) + { + role = new ApplicationRole(); + role.Id = roleId; + role.Name = roleName; + role.NormalizedName = roleName.ToUpper(); + } + + return role; + + } + + /// + /// Gets the ApplicationRole given the role name + /// + /// + /// + public ApplicationRole? GetRoleByName(string roleName) + { + var roleId = GetRoleId(roleName); + ApplicationRole role = null; + + if (roleId != null) + { + role = new ApplicationRole(); + role.Id = roleId; + role.Name = roleName; + role.NormalizedName = roleName.ToUpper(); + } + + return role; + } + + public int Update(ApplicationRole role) + { + string commandText = "Update Roles set Name = @name where Id = @id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@id", role.Id); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + public List GetRoles() + { + List roles = new List(); + + string commandText = "Select Name from Roles"; + + var rows = _database.ExecuteCMDDict(commandText); + foreach(Dictionary row in rows) + { + ApplicationRole role = (ApplicationRole)Activator.CreateInstance(typeof(ApplicationRole)); + role.Id = (string)row["Id"]; + role.Name = (string)row["Name"]; + role.NormalizedName = ((string)row["Name"]).ToUpper(); + roles.Add(role); + } + + return roles; + } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/UserClaimsTable.cs b/gaseous-server/Classes/Auth/Classes/UserClaimsTable.cs new file mode 100644 index 0000000..5940e10 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/UserClaimsTable.cs @@ -0,0 +1,95 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Security.Claims; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; + +namespace Authentication +{ + /// + /// Class that represents the UserClaims table in the MySQL Database + /// + public class UserClaimsTable + { + private Database _database; + + /// + /// Constructor that takes a MySQLDatabase instance + /// + /// + public UserClaimsTable(Database database) + { + _database = database; + } + + /// + /// Returns a ClaimsIdentity instance given a userId + /// + /// The user's id + /// + public ClaimsIdentity FindByUserId(string userId) + { + ClaimsIdentity claims = new ClaimsIdentity(); + string commandText = "Select * from UserClaims where UserId = @userId"; + Dictionary parameters = new Dictionary() { { "@UserId", userId } }; + + var rows = _database.ExecuteCMD(commandText, parameters).Rows; + foreach (DataRow row in rows) + { + Claim claim = new Claim((string)row["ClaimType"], (string)row["ClaimValue"]); + claims.AddClaim(claim); + } + + return claims; + } + + /// + /// Deletes all claims from a user given a userId + /// + /// The user's id + /// + public int Delete(string userId) + { + string commandText = "Delete from UserClaims where UserId = @userId"; + Dictionary parameters = new Dictionary(); + parameters.Add("userId", userId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Inserts a new claim in UserClaims table + /// + /// User's claim to be added + /// User's id + /// + public int Insert(Claim userClaim, string userId) + { + string commandText = "Insert into UserClaims (ClaimValue, ClaimType, UserId) values (@value, @type, @userId)"; + Dictionary parameters = new Dictionary(); + parameters.Add("value", userClaim.Value); + parameters.Add("type", userClaim.Type); + parameters.Add("userId", userId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Deletes a claim from a user + /// + /// The user to have a claim deleted + /// A claim to be deleted from user + /// + public int Delete(IdentityUser user, Claim claim) + { + string commandText = "Delete from UserClaims where UserId = @userId and @ClaimValue = @value and ClaimType = @type"; + Dictionary parameters = new Dictionary(); + parameters.Add("userId", user.Id); + parameters.Add("value", claim.Value); + parameters.Add("type", claim.Type); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs b/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs new file mode 100644 index 0000000..2837b36 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/UserLoginsTable.cs @@ -0,0 +1,117 @@ +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; +using System.Collections.Generic; +using System.Data; + +namespace Authentication +{ + /// + /// Class that represents the UserLogins table in the MySQL Database + /// + public class UserLoginsTable + { + private Database _database; + + /// + /// Constructor that takes a MySQLDatabase instance + /// + /// + public UserLoginsTable(Database database) + { + _database = database; + } + + /// + /// Deletes a login from a user in the UserLogins table + /// + /// User to have login deleted + /// Login to be deleted from user + /// + public int Delete(IdentityUser user, UserLoginInfo login) + { + string commandText = "Delete from UserLogins where UserId = @userId and LoginProvider = @loginProvider and ProviderKey = @providerKey"; + Dictionary parameters = new Dictionary(); + parameters.Add("UserId", user.Id); + parameters.Add("loginProvider", login.LoginProvider); + parameters.Add("providerKey", login.ProviderKey); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Deletes all Logins from a user in the UserLogins table + /// + /// The user's id + /// + public int Delete(string userId) + { + string commandText = "Delete from UserLogins where UserId = @userId"; + Dictionary parameters = new Dictionary(); + parameters.Add("UserId", userId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Inserts a new login in the UserLogins table + /// + /// User to have new login added + /// Login to be added + /// + public int Insert(IdentityUser user, UserLoginInfo login) + { + string commandText = "Insert into UserLogins (LoginProvider, ProviderKey, UserId) values (@loginProvider, @providerKey, @userId)"; + Dictionary parameters = new Dictionary(); + parameters.Add("loginProvider", login.LoginProvider); + parameters.Add("providerKey", login.ProviderKey); + parameters.Add("userId", user.Id); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Return a userId given a user's login + /// + /// The user's login info + /// + public string? FindUserIdByLogin(UserLoginInfo userLogin) + { + string commandText = "Select UserId from UserLogins where LoginProvider = @loginProvider and ProviderKey = @providerKey"; + Dictionary parameters = new Dictionary(); + parameters.Add("loginProvider", userLogin.LoginProvider); + parameters.Add("providerKey", userLogin.ProviderKey); + + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Returns a list of user's logins + /// + /// The user's id + /// + public List FindByUserId(string userId) + { + List logins = new List(); + string commandText = "Select * from UserLogins where UserId = @userId"; + Dictionary parameters = new Dictionary() { { "@userId", userId } }; + + var rows = _database.ExecuteCMD(commandText, parameters).Rows; + foreach (DataRow row in rows) + { + var login = new UserLoginInfo((string)row["LoginProvider"], (string)row["ProviderKey"], (string)row["LoginProvider"]); + logins.Add(login); + } + + return logins; + } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs b/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs new file mode 100644 index 0000000..14f8e2a --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/UserRoleTable.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Data; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; + +namespace Authentication +{ + /// + /// Class that represents the UserRoles table in the MySQL Database + /// + public class UserRolesTable + { + private Database _database; + + /// + /// Constructor that takes a MySQLDatabase instance + /// + /// + public UserRolesTable(Database database) + { + _database = database; + } + + /// + /// Returns a list of user's roles + /// + /// The user's id + /// + public List FindByUserId(string userId) + { + List roles = new List(); + string commandText = "Select Roles.Name from UserRoles, Roles where UserRoles.UserId = @userId and UserRoles.RoleId = Roles.Id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@userId", userId); + + var rows = _database.ExecuteCMD(commandText, parameters).Rows; + foreach(DataRow row in rows) + { + roles.Add((string)row["Name"]); + } + + return roles; + } + + /// + /// Deletes all roles from a user in the UserRoles table + /// + /// The user's id + /// + public int Delete(string userId) + { + string commandText = "Delete from UserRoles where UserId = @userId"; + Dictionary parameters = new Dictionary(); + parameters.Add("UserId", userId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + public int DeleteUserFromRole(string userId, string roleId) + { + string commandText = "Delete from UserRoles where UserId = @userId and RoleId = @roleId"; + Dictionary parameters = new Dictionary(); + parameters.Add("userId", userId); + parameters.Add("roleId", roleId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + + /// + /// Inserts a new role for a user in the UserRoles table + /// + /// The User + /// The Role's id + /// + public int Insert(IdentityUser user, string roleId) + { + string commandText = "Insert into UserRoles (UserId, RoleId) values (@userId, @roleId)"; + Dictionary parameters = new Dictionary(); + parameters.Add("userId", user.Id); + parameters.Add("roleId", roleId); + + return (int)_database.ExecuteNonQuery(commandText, parameters); + } + } +} diff --git a/gaseous-server/Classes/Auth/Classes/UserStore.cs b/gaseous-server/Classes/Auth/Classes/UserStore.cs new file mode 100644 index 0000000..348ebe5 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/UserStore.cs @@ -0,0 +1,616 @@ +using System; +using System.Security.Claims; +using System.Threading.Tasks; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; +using MySqlConnector; + +namespace Authentication +{ + public class UserStore : + IUserStore, + IUserRoleStore, + IUserLoginStore, + IUserClaimStore, + IUserPasswordStore, + IUserSecurityStampStore, + IQueryableUserStore, + IUserEmailStore, + IUserPhoneNumberStore, + IUserTwoFactorStore, + IUserLockoutStore + { + private Database database; + + private UserTable userTable; + private RoleTable roleTable; + private UserRolesTable userRolesTable; + private UserLoginsTable userLoginsTable; + private UserClaimsTable userClaimsTable; + + public UserStore() + { + database = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + userTable = new UserTable(database); + roleTable = new RoleTable(database); + userRolesTable = new UserRolesTable(database); + userLoginsTable = new UserLoginsTable(database); + userClaimsTable = new UserClaimsTable(database); + } + + public UserStore(Database database) + { + this.database = database; + userTable = new UserTable(database); + roleTable = new RoleTable(database); + userRolesTable = new UserRolesTable(database); + userLoginsTable = new UserLoginsTable(database); + userClaimsTable = new UserClaimsTable(database); + } + + public IQueryable Users + { + get + { + List users = userTable.GetUsers(); + return users.AsQueryable(); + } + } + + public Task AddClaimsAsync(ApplicationUser user, IEnumerable claims, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (claims == null) + { + throw new ArgumentNullException("user"); + } + + foreach (Claim claim in claims) + { + userClaimsTable.Insert(claim, user.Id); + } + + return Task.FromResult(null); + } + + public Task AddLoginAsync(ApplicationUser user, UserLoginInfo login, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (login == null) + { + throw new ArgumentNullException("login"); + } + + userLoginsTable.Insert(user, login); + + return Task.FromResult(null); + } + + public Task AddToRoleAsync(ApplicationUser user, string roleName, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (string.IsNullOrEmpty(roleName)) + { + throw new ArgumentException("Argument cannot be null or empty: roleName."); + } + + string roleId = roleTable.GetRoleId(roleName); + if (!string.IsNullOrEmpty(roleId)) + { + userRolesTable.Insert(user, roleId); + } + + return Task.FromResult(null); + } + + public Task CreateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + userTable.Insert(user); + + return Task.FromResult(IdentityResult.Success); + } + + public Task DeleteAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user != null) + { + userTable.Delete(user); + } + + return Task.FromResult(IdentityResult.Success); + } + + public void Dispose() + { + if (database != null) + { + database = null; + } + } + + public Task FindByEmailAsync(string normalizedEmail, CancellationToken cancellationToken) + { + if (String.IsNullOrEmpty(normalizedEmail)) + { + throw new ArgumentNullException("email"); + } + + ApplicationUser result = userTable.GetUserByEmail(normalizedEmail) as ApplicationUser; + if (result != null) + { + return Task.FromResult(result); + } + + return Task.FromResult(null); + } + + public Task FindByIdAsync(string userId, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(userId)) + { + throw new ArgumentException("Null or empty argument: userId"); + } + + ApplicationUser result = userTable.GetUserById(userId) as ApplicationUser; + if (result != null) + { + return Task.FromResult(result); + } + + return Task.FromResult(null); + } + + public Task FindByLoginAsync(string loginProvider, string providerKey, CancellationToken cancellationToken) + { + if (loginProvider == null || providerKey == null) + { + throw new ArgumentNullException("login"); + } + + UserLoginInfo login = new UserLoginInfo(loginProvider, providerKey, loginProvider); + + var userId = userLoginsTable.FindUserIdByLogin(login); + if (userId != null) + { + ApplicationUser user = userTable.GetUserById(userId) as ApplicationUser; + if (user != null) + { + return Task.FromResult(user); + } + } + + return Task.FromResult(null); + } + + public Task FindByNameAsync(string normalizedUserName, CancellationToken cancellationToken) + { + if (string.IsNullOrEmpty(normalizedUserName)) + { + throw new ArgumentException("Null or empty argument: normalizedUserName"); + } + + List result = userTable.GetUserByName(normalizedUserName) as List; + + // Should I throw if > 1 user? + if (result != null && result.Count == 1) + { + return Task.FromResult(result[0]); + } + + return Task.FromResult(null); + } + + public Task GetAccessFailedCountAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.AccessFailedCount); + } + + public Task> GetClaimsAsync(ApplicationUser user, CancellationToken cancellationToken) + { + ClaimsIdentity identity = userClaimsTable.FindByUserId(user.Id); + + return Task.FromResult>(identity.Claims.ToList()); + } + + public Task GetEmailAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.Email); + } + + public Task GetEmailConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.EmailConfirmed); + } + + public Task GetLockoutEnabledAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.LockoutEnabled); + } + + public Task GetLockoutEndDateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user.LockoutEnd.HasValue) + { + return Task.FromResult((DateTimeOffset?)user.LockoutEnd.Value); + } + else + { + return Task.FromResult((DateTimeOffset?)new DateTimeOffset()); + } + } + + public Task> GetLoginsAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + List logins = userLoginsTable.FindByUserId(user.Id); + if (logins != null) + { + return Task.FromResult>(logins); + } + + return Task.FromResult>(null); + } + + public Task GetNormalizedEmailAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.NormalizedEmail); + } + + public Task GetNormalizedUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user != null) + { + return Task.FromResult(userTable.GetUserName(user.Id)); + } + + return Task.FromResult(null); + } + + public Task GetPasswordHashAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user != null) + { + return Task.FromResult(userTable.GetPasswordHash(user.Id)); + } + + return Task.FromResult(null); + } + + public Task GetPhoneNumberAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.PhoneNumber); + } + + public Task GetPhoneNumberConfirmedAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.PhoneNumberConfirmed); + } + + public Task> GetRolesAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + List roles = userRolesTable.FindByUserId(user.Id); + { + if (roles != null) + { + return Task.FromResult>(roles); + } + } + + return Task.FromResult>(null); + } + + public Task GetSecurityStampAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.SecurityStamp); + } + + public Task GetTwoFactorEnabledAsync(ApplicationUser user, CancellationToken cancellationToken) + { + return Task.FromResult(user.TwoFactorEnabled); + } + + public Task GetUserIdAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user != null) + { + return Task.FromResult(userTable.GetUserId(user.NormalizedUserName)); + } + + return Task.FromResult(null); + } + + public Task GetUserNameAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user != null) + { + //return Task.FromResult(userTable.GetUserName(user.Id)); + return Task.FromResult(user.UserName); + } + + return Task.FromResult(null); + } + + public Task> GetUsersForClaimAsync(Claim claim, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task> GetUsersInRoleAsync(string roleName, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + } + + public Task HasPasswordAsync(ApplicationUser user, CancellationToken cancellationToken) + { + var hasPassword = !string.IsNullOrEmpty(userTable.GetPasswordHash(user.Id)); + + return Task.FromResult(Boolean.Parse(hasPassword.ToString())); + } + + public Task IncrementAccessFailedCountAsync(ApplicationUser user, CancellationToken cancellationToken) + { + user.AccessFailedCount++; + userTable.Update(user); + + return Task.FromResult(user.AccessFailedCount); + } + + public Task IsInRoleAsync(ApplicationUser user, string roleName, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (string.IsNullOrEmpty(roleName)) + { + throw new ArgumentNullException("role"); + } + + List roles = userRolesTable.FindByUserId(user.Id); + { + if (roles != null) + { + foreach (string role in roles) + { + if (role.ToUpper() == roleName.ToUpper()) + { + return Task.FromResult(true); + } + } + } + } + + return Task.FromResult(false); + } + + public Task RemoveClaimsAsync(ApplicationUser user, IEnumerable claims, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (claims == null) + { + throw new ArgumentNullException("claim"); + } + + foreach (Claim claim in claims) + { + userClaimsTable.Delete(user, claim); + } + + return Task.FromResult(null); + } + + public Task RemoveFromRoleAsync(ApplicationUser user, string roleName, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (roleName == null) + { + throw new ArgumentNullException("role"); + } + + IdentityRole? role = roleTable.GetRoleByName(roleName); + + if (role != null) + { + userRolesTable.DeleteUserFromRole(user.Id, role.Id); + } + + return Task.FromResult(null); + } + + public Task RemoveLoginAsync(ApplicationUser user, string loginProvider, string providerKey, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (loginProvider == null || providerKey == null) + { + throw new ArgumentNullException("login"); + } + + UserLoginInfo login = new UserLoginInfo(loginProvider, providerKey, loginProvider); + + userLoginsTable.Delete(user, login); + + return Task.FromResult(null); + } + + public Task ReplaceClaimAsync(ApplicationUser user, Claim claim, Claim newClaim, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + if (claim == null || newClaim == null) + { + throw new ArgumentNullException("claim"); + } + + userClaimsTable.Delete(user, claim); + userClaimsTable.Insert(newClaim, user.Id); + + return Task.FromResult(null); + } + + public Task ResetAccessFailedCountAsync(ApplicationUser user, CancellationToken cancellationToken) + { + user.AccessFailedCount = 0; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetEmailAsync(ApplicationUser user, string? email, CancellationToken cancellationToken) + { + user.Email = email; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetEmailConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken) + { + user.EmailConfirmed = confirmed; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetLockoutEnabledAsync(ApplicationUser user, bool enabled, CancellationToken cancellationToken) + { + user.LockoutEnabled = enabled; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetLockoutEndDateAsync(ApplicationUser user, DateTimeOffset? lockoutEnd, CancellationToken cancellationToken) + { + user.LockoutEnd = lockoutEnd; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetNormalizedEmailAsync(ApplicationUser user, string? normalizedEmail, CancellationToken cancellationToken) + { + user.NormalizedEmail = normalizedEmail; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetNormalizedUserNameAsync(ApplicationUser user, string? normalizedName, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + user.NormalizedUserName = normalizedName; + userTable.Update(user); + + return Task.FromResult(IdentityResult.Success); + } + + public Task SetPasswordHashAsync(ApplicationUser user, string? passwordHash, CancellationToken cancellationToken) + { + user.PasswordHash = passwordHash; + + return Task.FromResult(null); + } + + public Task SetPhoneNumberAsync(ApplicationUser user, string? phoneNumber, CancellationToken cancellationToken) + { + user.PhoneNumber = phoneNumber; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetPhoneNumberConfirmedAsync(ApplicationUser user, bool confirmed, CancellationToken cancellationToken) + { + user.PhoneNumberConfirmed = confirmed; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetSecurityStampAsync(ApplicationUser user, string stamp, CancellationToken cancellationToken) + { + user.SecurityStamp = stamp; + + return Task.FromResult(0); + } + + public Task SetTwoFactorEnabledAsync(ApplicationUser user, bool enabled, CancellationToken cancellationToken) + { + user.TwoFactorEnabled = enabled; + userTable.Update(user); + + return Task.FromResult(0); + } + + public Task SetUserNameAsync(ApplicationUser user, string? userName, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + user.UserName = userName; + userTable.Update(user); + + return Task.FromResult(IdentityResult.Success); + } + + public Task UpdateAsync(ApplicationUser user, CancellationToken cancellationToken) + { + if (user == null) + { + throw new ArgumentNullException("user"); + } + + userTable.Update(user); + + return Task.FromResult(IdentityResult.Success); + } + } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Classes/UserTable.cs b/gaseous-server/Classes/Auth/Classes/UserTable.cs new file mode 100644 index 0000000..ed5e496 --- /dev/null +++ b/gaseous-server/Classes/Auth/Classes/UserTable.cs @@ -0,0 +1,371 @@ +using System; +using System.Collections.Generic; +using System.Data; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Identity; + +namespace Authentication +{ + /// + /// Class that represents the Users table in the MySQL Database + /// + public class UserTable + where TUser :ApplicationUser + { + private Database _database; + + /// + /// Constructor that takes a MySQLDatabase instance + /// + /// + public UserTable(Database database) + { + _database = database; + } + + /// + /// Returns the user's name given a user id + /// + /// + /// + public string? GetUserName(string userId) + { + string commandText = "Select NormalizedUserName from Users where Id = @id"; + Dictionary parameters = new Dictionary() { { "@id", userId } }; + + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Returns a User ID given a user name + /// + /// The user's name + /// + public string? GetUserId(string normalizedUserName) + { + string commandText = "Select Id from Users where NormalizedUserName = @name"; + Dictionary parameters = new Dictionary() { { "@name", normalizedUserName } }; + + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Returns an TUser given the user's id + /// + /// The user's id + /// + public TUser GetUserById(string userId) + { + TUser user = null; + string commandText = "Select * from Users where Id = @id"; + Dictionary parameters = new Dictionary() { { "@id", userId } }; + + var rows = _database.ExecuteCMDDict(commandText, parameters); + if (rows != null && rows.Count == 1) + { + Dictionary row = rows[0]; + user = (TUser)Activator.CreateInstance(typeof(TUser)); + user.Id = (string)row["Id"]; + user.UserName = (string?)row["UserName"]; + user.PasswordHash = (string?)(string.IsNullOrEmpty((string?)row["PasswordHash"]) ? null : row["PasswordHash"]); + user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); + user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); + user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); + user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; + user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); + user.NormalizedUserName = (string?)(string.IsNullOrEmpty((string?)row["NormalizedUserName"]) ? null : row["NormalizedUserName"]); + user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; + user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); + user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.SecurityProfile = GetSecurityProfile(user); + } + + return user; + } + + /// + /// Returns a list of TUser instances given a user name + /// + /// User's name + /// + public List GetUserByName(string normalizedUserName) + { + List users = new List(); + string commandText = "Select * from Users where NormalizedEmail = @name"; + Dictionary parameters = new Dictionary() { { "@name", normalizedUserName } }; + + var rows = _database.ExecuteCMDDict(commandText, parameters); + foreach(Dictionary row in rows) + { + TUser user = (TUser)Activator.CreateInstance(typeof(TUser)); + user.Id = (string)row["Id"]; + user.UserName = (string?)row["UserName"]; + user.PasswordHash = (string?)(string.IsNullOrEmpty((string?)row["PasswordHash"]) ? null : row["PasswordHash"]); + user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); + user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); + user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); + user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; + user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); + user.NormalizedUserName = (string?)(string.IsNullOrEmpty((string?)row["NormalizedUserName"]) ? null : row["NormalizedUserName"]); + user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; + user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); + user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.SecurityProfile = GetSecurityProfile(user); + users.Add(user); + } + + return users; + } + + public List GetUsers() + { + List users = new List(); + string commandText = "Select * from Users order by NormalizedUserName"; + + var rows = _database.ExecuteCMDDict(commandText); + foreach(Dictionary row in rows) + { + TUser user = (TUser)Activator.CreateInstance(typeof(TUser)); + user.Id = (string)row["Id"]; + user.UserName = (string?)row["UserName"]; + user.PasswordHash = (string?)(string.IsNullOrEmpty((string?)row["PasswordHash"]) ? null : row["PasswordHash"]); + user.SecurityStamp = (string?)(string.IsNullOrEmpty((string?)row["SecurityStamp"]) ? null : row["SecurityStamp"]); + user.ConcurrencyStamp = (string?)(string.IsNullOrEmpty((string?)row["ConcurrencyStamp"]) ? null : row["ConcurrencyStamp"]); + user.Email = (string?)(string.IsNullOrEmpty((string?)row["Email"]) ? null : row["Email"]); + user.EmailConfirmed = row["EmailConfirmed"] == "1" ? true:false; + user.PhoneNumber = (string?)(string.IsNullOrEmpty((string?)row["PhoneNumber"]) ? null : row["PhoneNumber"]); + user.PhoneNumberConfirmed = row["PhoneNumberConfirmed"] == "1" ? true : false; + user.NormalizedEmail = (string?)(string.IsNullOrEmpty((string?)row["NormalizedEmail"]) ? null : row["NormalizedEmail"]); + user.NormalizedUserName = (string?)(string.IsNullOrEmpty((string?)row["NormalizedUserName"]) ? null : row["NormalizedUserName"]); + user.LockoutEnabled = row["LockoutEnabled"] == "1" ? true : false; + user.LockoutEnd = string.IsNullOrEmpty((string?)row["LockoutEnd"]) ? DateTime.Now : DateTime.Parse((string?)row["LockoutEnd"]); + user.AccessFailedCount = string.IsNullOrEmpty((string?)row["AccessFailedCount"]) ? 0 : int.Parse((string?)row["AccessFailedCount"]); + user.TwoFactorEnabled = row["TwoFactorEnabled"] == "1" ? true:false; + user.SecurityProfile = GetSecurityProfile(user); + users.Add(user); + } + + return users; + } + + public TUser GetUserByEmail(string email) + { + List users = GetUserByName(email); + if (users.Count == 0) + { + return null; + } + else + { + return users[0]; + } + } + + /// + /// Return the user's password hash + /// + /// The user's id + /// + public string GetPasswordHash(string userId) + { + string commandText = "Select PasswordHash from Users where Id = @id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@id", userId); + + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Sets the user's password hash + /// + /// + /// + /// + public int SetPasswordHash(string userId, string passwordHash) + { + string commandText = "Update Users set PasswordHash = @pwdHash where Id = @id"; + Dictionary parameters = new Dictionary(); + parameters.Add("@pwdHash", passwordHash); + parameters.Add("@id", userId); + + return _database.ExecuteCMD(commandText, parameters).Rows.Count; + } + + /// + /// Returns the user's security stamp + /// + /// + /// + public string GetSecurityStamp(string userId) + { + string commandText = "Select SecurityStamp from Users where Id = @id"; + Dictionary parameters = new Dictionary() { { "@id", userId } }; + DataTable table = _database.ExecuteCMD(commandText, parameters); + + if (table.Rows.Count == 0) + { + return null; + } + else + { + return (string)table.Rows[0][0]; + } + } + + /// + /// Inserts a new user in the Users table + /// + /// + /// + public int Insert(TUser user) + { + string commandText = @"Insert into Users (UserName, Id, PasswordHash, SecurityStamp, ConcurrencyStamp, Email, EmailConfirmed, PhoneNumber, PhoneNumberConfirmed, NormalizedEmail, NormalizedUserName, AccessFailedCount, LockoutEnabled, LockoutEnd, TwoFactorEnabled) values (@name, @id, @pwdHash, @SecStamp, @concurrencystamp, @email ,@emailconfirmed ,@phonenumber, @phonenumberconfirmed, @normalizedemail, @normalizedusername, @accesscount, @lockoutenabled, @lockoutenddate, @twofactorenabled);"; + Dictionary parameters = new Dictionary(); + parameters.Add("@name", user.UserName); + parameters.Add("@id", user.Id); + parameters.Add("@pwdHash", user.PasswordHash); + parameters.Add("@SecStamp", user.SecurityStamp); + parameters.Add("@concurrencystamp", user.ConcurrencyStamp); + parameters.Add("@email", user.Email); + parameters.Add("@emailconfirmed", user.EmailConfirmed); + parameters.Add("@phonenumber", user.PhoneNumber); + parameters.Add("@phonenumberconfirmed", user.PhoneNumberConfirmed); + parameters.Add("@normalizedemail", user.NormalizedEmail); + parameters.Add("@normalizedusername", user.NormalizedUserName); + parameters.Add("@accesscount", user.AccessFailedCount); + parameters.Add("@lockoutenabled", user.LockoutEnabled); + parameters.Add("@lockoutenddate", user.LockoutEnd); + parameters.Add("@twofactorenabled", user.TwoFactorEnabled); + + // set default security profile + SetSecurityProfile(user, new SecurityProfileViewModel()); + + return _database.ExecuteCMD(commandText, parameters).Rows.Count; + } + + /// + /// Deletes a user from the Users table + /// + /// The user's id + /// + private int Delete(string userId) + { + string commandText = "Delete from Users where Id = @userId"; + Dictionary parameters = new Dictionary(); + parameters.Add("@userId", userId); + + return _database.ExecuteCMD(commandText, parameters).Rows.Count; + } + + /// + /// Deletes a user from the Users table + /// + /// + /// + public int Delete(TUser user) + { + return Delete(user.Id); + } + + /// + /// Updates a user in the Users table + /// + /// + /// + public int Update(TUser user) + { + string commandText = @"Update Users set UserName = @userName, PasswordHash = @pwdHash, SecurityStamp = @secStamp, ConcurrencyStamp = @concurrencystamp, Email = @email, EmailConfirmed = @emailconfirmed, PhoneNumber = @phonenumber, PhoneNumberConfirmed = @phonenumberconfirmed, NormalizedEmail = @normalizedemail, NormalizedUserName = @normalizedusername, AccessFailedCount = @accesscount, LockoutEnabled = @lockoutenabled, LockoutEnd = @lockoutenddate, TwoFactorEnabled=@twofactorenabled WHERE Id = @userId;"; + Dictionary parameters = new Dictionary(); + parameters.Add("@userId", user.Id); + parameters.Add("@userName", user.UserName); + parameters.Add("@pwdHash", user.PasswordHash); + parameters.Add("@SecStamp", user.SecurityStamp); + parameters.Add("@concurrencystamp", user.ConcurrencyStamp); + parameters.Add("@email", user.Email); + parameters.Add("@emailconfirmed", user.EmailConfirmed); + parameters.Add("@phonenumber", user.PhoneNumber); + parameters.Add("@phonenumberconfirmed", user.PhoneNumberConfirmed); + parameters.Add("@normalizedemail", user.NormalizedEmail); + parameters.Add("@normalizedusername", user.NormalizedUserName); + parameters.Add("@accesscount", user.AccessFailedCount); + parameters.Add("@lockoutenabled", user.LockoutEnabled); + parameters.Add("@lockoutenddate", user.LockoutEnd); + parameters.Add("@twofactorenabled", user.TwoFactorEnabled); + + // set the security profile + SetSecurityProfile(user, user.SecurityProfile); + + return _database.ExecuteCMD(commandText, parameters).Rows.Count; + } + + private SecurityProfileViewModel GetSecurityProfile(TUser user) + { + string sql = "SELECT SecurityProfile FROM users WHERE Id=@Id;"; + Dictionary dbDict = new Dictionary(); + dbDict.Add("Id", user.Id); + + List> data = _database.ExecuteCMDDict(sql, dbDict); + if (data.Count == 0) + { + // no saved profile - return the default one + return new SecurityProfileViewModel(); + } + else + { + string? securityProfileString = (string?)data[0]["SecurityProfile"]; + if (securityProfileString != null && securityProfileString != "null") + { + SecurityProfileViewModel securityProfile = Newtonsoft.Json.JsonConvert.DeserializeObject(securityProfileString); + return securityProfile; + } + else + { + return new SecurityProfileViewModel(); + } + } + } + + private int SetSecurityProfile(TUser user, SecurityProfileViewModel securityProfile) + { + string commandText = "UPDATE users SET SecurityProfile=@SecurityProfile WHERE Id=@Id;"; + Dictionary parameters = new Dictionary(); + parameters.Add("Id", user.Id); + parameters.Add("SecurityProfile", Newtonsoft.Json.JsonConvert.SerializeObject(securityProfile)); + + return _database.ExecuteCMD(commandText, parameters).Rows.Count; + } + } +} diff --git a/gaseous-server/Classes/Auth/Models/AccountViewModels.cs b/gaseous-server/Classes/Auth/Models/AccountViewModels.cs new file mode 100644 index 0000000..a10bcef --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/AccountViewModels.cs @@ -0,0 +1,100 @@ +using System.ComponentModel.DataAnnotations; + +namespace Authentication +{ + public class ExternalLoginConfirmationViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + } + + public class ManageUserViewModel + { + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class LoginViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } + } + + public class RegisterViewModel + { + [Required] + [DataType(DataType.Text)] + [Display(Name = "User name")] + public string UserName { get; set; } + + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + } + + public class ResetPasswordViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "Password")] + public string Password { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm password")] + [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } + + public string Code { get; set; } + } + + public class ForgotPasswordViewModel + { + [Required] + [EmailAddress] + [Display(Name = "Email")] + public string Email { get; set; } + } +} diff --git a/gaseous-server/Classes/Auth/Models/AddPhoneNumberViewModel.cs b/gaseous-server/Classes/Auth/Models/AddPhoneNumberViewModel.cs new file mode 100644 index 0000000..05aad08 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/AddPhoneNumberViewModel.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class AddPhoneNumberViewModel +{ + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/ChangePasswordViewModel.cs b/gaseous-server/Classes/Auth/Models/ChangePasswordViewModel.cs new file mode 100644 index 0000000..e25188f --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/ChangePasswordViewModel.cs @@ -0,0 +1,25 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class ChangePasswordViewModel +{ + [Required] + [DataType(DataType.Password)] + [Display(Name = "Current password")] + public string OldPassword { get; set; } + + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 10)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/DisplayRecoveryCodesViewModel.cs b/gaseous-server/Classes/Auth/Models/DisplayRecoveryCodesViewModel.cs new file mode 100644 index 0000000..d6245b5 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/DisplayRecoveryCodesViewModel.cs @@ -0,0 +1,13 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class DisplayRecoveryCodesViewModel +{ + [Required] + public IEnumerable Codes { get; set; } + +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/IndexViewModel.cs b/gaseous-server/Classes/Auth/Models/IndexViewModel.cs new file mode 100644 index 0000000..f7f2365 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/IndexViewModel.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Identity; + +namespace Authentication; + +public class IndexViewModel +{ + public bool HasPassword { get; set; } + + public IList Logins { get; set; } + + public string PhoneNumber { get; set; } + + public bool TwoFactor { get; set; } + + public bool BrowserRemembered { get; set; } + + public string AuthenticatorKey { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/ManageLoginsViewModel.cs b/gaseous-server/Classes/Auth/Models/ManageLoginsViewModel.cs new file mode 100644 index 0000000..c050be8 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/ManageLoginsViewModel.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Identity; + +namespace Authentication; + +public class ManageLoginsViewModel +{ + public IList CurrentLogins { get; set; } + + public IList OtherLogins { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs b/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs new file mode 100644 index 0000000..3a1843e --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/ProfileViewModel.cs @@ -0,0 +1,46 @@ +namespace Authentication +{ + public class ProfileBasicViewModel + { + public string UserId { get; set; } + public string UserName { get; set; } + public string EmailAddress { get; set; } + public List Roles { get; set; } + public SecurityProfileViewModel SecurityProfile { get; set; } + public string HighestRole { + get + { + string _highestRole = ""; + foreach (string role in Roles) + { + switch (role) + { + case "Admin": + // there is no higher + _highestRole = role; + break; + case "Gamer": + // only one high is Admin, so check for that + if (_highestRole != "Admin") + { + _highestRole = role; + } + break; + case "Player": + // make sure _highestRole isn't already set to Gamer or Admin + if (_highestRole != "Admin" && _highestRole != "Gamer") + { + _highestRole = role; + } + break; + default: + _highestRole = "Player"; + break; + } + } + + return _highestRole; + } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/RemoveLoginViewModel.cs b/gaseous-server/Classes/Auth/Models/RemoveLoginViewModel.cs new file mode 100644 index 0000000..4b1a09b --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/RemoveLoginViewModel.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Authentication; + +public class RemoveLoginViewModel +{ + public string LoginProvider { get; set; } + public string ProviderKey { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/SecurityProfileViewModel.cs b/gaseous-server/Classes/Auth/Models/SecurityProfileViewModel.cs new file mode 100644 index 0000000..da493a2 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/SecurityProfileViewModel.cs @@ -0,0 +1,18 @@ +using System.ComponentModel.DataAnnotations; + +namespace Authentication +{ + public class SecurityProfileViewModel + { + public AgeRestrictionItem AgeRestrictionPolicy { get; set; } = new AgeRestrictionItem{ + MaximumAgeRestriction = "Adult", + IncludeUnrated = true + }; + + public class AgeRestrictionItem + { + public string MaximumAgeRestriction { get; set; } + public bool IncludeUnrated { get; set; } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/SendCodeViewModel.cs b/gaseous-server/Classes/Auth/Models/SendCodeViewModel.cs new file mode 100644 index 0000000..fd1f011 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/SendCodeViewModel.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace Authentication; + +public class SendCodeViewModel +{ + public string SelectedProvider { get; set; } + + public ICollection Providers { get; set; } + + public string ReturnUrl { get; set; } + + public bool RememberMe { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/SetPasswordViewModel.cs b/gaseous-server/Classes/Auth/Models/SetPasswordViewModel.cs new file mode 100644 index 0000000..4b8a62c --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/SetPasswordViewModel.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class SetPasswordViewModel +{ + [Required] + [StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)] + [DataType(DataType.Password)] + [Display(Name = "New password")] + public string NewPassword { get; set; } + + [DataType(DataType.Password)] + [Display(Name = "Confirm new password")] + [Compare("NewPassword", ErrorMessage = "The new password and confirmation password do not match.")] + public string ConfirmPassword { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/UseRecoveryCodeViewModel.cs b/gaseous-server/Classes/Auth/Models/UseRecoveryCodeViewModel.cs new file mode 100644 index 0000000..f3ec77b --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/UseRecoveryCodeViewModel.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class UseRecoveryCodeViewModel +{ + [Required] + public string Code { get; set; } + + public string ReturnUrl { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/UserViewModel.cs b/gaseous-server/Classes/Auth/Models/UserViewModel.cs new file mode 100644 index 0000000..cedbb8a --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/UserViewModel.cs @@ -0,0 +1,47 @@ +namespace Authentication +{ + public class UserViewModel + { + public string Id { get; set; } + public string EmailAddress { get; set; } + public bool LockoutEnabled { get; set; } + public DateTimeOffset? LockoutEnd { get; set; } + public List Roles { get; set; } + public SecurityProfileViewModel SecurityProfile { get; set; } + public string HighestRole { + get + { + string _highestRole = ""; + foreach (string role in Roles) + { + switch (role) + { + case "Admin": + // there is no higher + _highestRole = role; + break; + case "Gamer": + // only one high is Admin, so check for that + if (_highestRole != "Admin") + { + _highestRole = role; + } + break; + case "Player": + // make sure _highestRole isn't already set to Gamer or Admin + if (_highestRole != "Admin" && _highestRole != "Gamer") + { + _highestRole = role; + } + break; + default: + _highestRole = "Player"; + break; + } + } + + return _highestRole; + } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/VerifyAuthenticatorCodeViewModel.cs b/gaseous-server/Classes/Auth/Models/VerifyAuthenticatorCodeViewModel.cs new file mode 100644 index 0000000..e1abe1c --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/VerifyAuthenticatorCodeViewModel.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class VerifyAuthenticatorCodeViewModel +{ + [Required] + public string Code { get; set; } + + public string ReturnUrl { get; set; } + + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/VerifyCodeViewModel.cs b/gaseous-server/Classes/Auth/Models/VerifyCodeViewModel.cs new file mode 100644 index 0000000..44dec11 --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/VerifyCodeViewModel.cs @@ -0,0 +1,23 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class VerifyCodeViewModel +{ + [Required] + public string Provider { get; set; } + + [Required] + public string Code { get; set; } + + public string ReturnUrl { get; set; } + + [Display(Name = "Remember this browser?")] + public bool RememberBrowser { get; set; } + + [Display(Name = "Remember me?")] + public bool RememberMe { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Auth/Models/VerifyPhoneNumberViewModel.cs b/gaseous-server/Classes/Auth/Models/VerifyPhoneNumberViewModel.cs new file mode 100644 index 0000000..0a7112f --- /dev/null +++ b/gaseous-server/Classes/Auth/Models/VerifyPhoneNumberViewModel.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.ComponentModel.DataAnnotations; + +namespace Authentication; + +public class VerifyPhoneNumberViewModel +{ + [Required] + public string Code { get; set; } + + [Required] + [Phone] + [Display(Name = "Phone number")] + public string PhoneNumber { get; set; } +} \ No newline at end of file diff --git a/gaseous-server/Classes/Database.cs b/gaseous-server/Classes/Database.cs index 5b67b39..9266d94 100644 --- a/gaseous-server/Classes/Database.cs +++ b/gaseous-server/Classes/Database.cs @@ -147,6 +147,50 @@ namespace gaseous_server.Classes return _ExecuteCMD(Command, Parameters, Timeout, ConnectionString); } + public List> ExecuteCMDDict(string Command) + { + Dictionary dbDict = new Dictionary(); + return _ExecuteCMDDict(Command, dbDict, 30, ""); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters) + { + return _ExecuteCMDDict(Command, Parameters, 30, ""); + } + + public List> ExecuteCMDDict(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + return _ExecuteCMDDict(Command, Parameters, Timeout, ConnectionString); + } + + private List> _ExecuteCMDDict(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + DataTable dataTable = _ExecuteCMD(Command, Parameters, Timeout, ConnectionString); + + // convert datatable to dictionary + List> rows = new List>(); + + foreach (DataRow dataRow in dataTable.Rows) + { + Dictionary row = new Dictionary(); + for (int i = 0; i < dataRow.Table.Columns.Count; i++) + { + string columnName = dataRow.Table.Columns[i].ColumnName; + if (dataRow[i] == System.DBNull.Value) + { + row.Add(columnName, null); + } + else + { + row.Add(columnName, dataRow[i].ToString()); + } + } + rows.Add(row); + } + + return rows; + } + private DataTable _ExecuteCMD(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") { if (ConnectionString == "") { ConnectionString = _ConnectionString; } @@ -160,6 +204,35 @@ namespace gaseous_server.Classes } } + public int ExecuteNonQuery(string Command) + { + Dictionary dbDict = new Dictionary(); + return _ExecuteNonQuery(Command, dbDict, 30, ""); + } + + public int ExecuteNonQuery(string Command, Dictionary Parameters) + { + return _ExecuteNonQuery(Command, Parameters, 30, ""); + } + + public int ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + return _ExecuteNonQuery(Command, Parameters, Timeout, ConnectionString); + } + + private int _ExecuteNonQuery(string Command, Dictionary Parameters, int Timeout = 30, string ConnectionString = "") + { + if (ConnectionString == "") { ConnectionString = _ConnectionString; } + switch (_ConnectorType) + { + case databaseType.MySql: + MySQLServerConnector conn = new MySQLServerConnector(ConnectionString); + return (int)conn.ExecNonQuery(Command, Parameters, Timeout); + default: + return 0; + } + } + public void ExecuteTransactionCMD(List CommandList, int Timeout = 60) { object conn; @@ -284,6 +357,47 @@ namespace gaseous_server.Classes return RetTable; } + public int ExecNonQuery(string SQL, Dictionary< string, object> Parameters, int Timeout) + { + int result = 0; + + Logging.Log(Logging.LogType.Debug, "Database", "Connecting to database", null, true); + MySqlConnection conn = new MySqlConnection(DBConn); + conn.Open(); + + MySqlCommand cmd = new MySqlCommand + { + Connection = conn, + CommandText = SQL, + CommandTimeout = Timeout + }; + + foreach (string Parameter in Parameters.Keys) + { + cmd.Parameters.AddWithValue(Parameter, Parameters[Parameter]); + } + + try + { + Logging.Log(Logging.LogType.Debug, "Database", "Executing sql: '" + SQL + "'", null, true); + if (Parameters.Count > 0) + { + string dictValues = string.Join(";", Parameters.Select(x => string.Join("=", x.Key, x.Value))); + Logging.Log(Logging.LogType.Debug, "Database", "Parameters: " + dictValues, null, true); + } + result = cmd.ExecuteNonQuery(); + } catch (Exception ex) { + Logging.Log(Logging.LogType.Critical, "Database", "Error while executing '" + SQL + "'", ex); + Trace.WriteLine("Error executing " + SQL); + Trace.WriteLine("Full exception: " + ex.ToString()); + } + + Logging.Log(Logging.LogType.Debug, "Database", "Closing database connection", null, true); + conn.Close(); + + return result; + } + public void TransactionExecCMD(List> Parameters, int Timeout) { var conn = new MySqlConnection(DBConn); diff --git a/gaseous-server/Classes/Metadata/AgeRating.cs b/gaseous-server/Classes/Metadata/AgeRating.cs index 1960874..d761fc6 100644 --- a/gaseous-server/Classes/Metadata/AgeRating.cs +++ b/gaseous-server/Classes/Metadata/AgeRating.cs @@ -1,5 +1,6 @@ using System; using System.Reflection; +using System.Text.Json.Serialization; using IGDB; using IGDB.Models; @@ -151,6 +152,202 @@ namespace gaseous_server.Classes.Metadata public AgeRatingTitle RatingTitle { get; set; } public string[] Descriptions { get; set; } } + + public class AgeGroups + { + public AgeGroups() + { + + } + + public static Dictionary> AgeGroupings + { + get + { + return new Dictionary>{ + { + "Adult", new List{ Adult_Item, Mature_Item, Teen_Item, Child_Item } + }, + { + "Mature", new List{ Mature_Item, Teen_Item, Child_Item } + }, + { + "Teen", new List{ Teen_Item, Child_Item } + }, + { + "Child", new List{ Child_Item } + } + }; + } + } + + public static Dictionary AgeGroupingsFlat + { + get + { + return new Dictionary{ + { + "Adult", Adult_Item + }, + { + "Mature", Mature_Item + }, + { + "Teen", Teen_Item + }, + { + "Child", Child_Item + } + }; + } + } + + public static List ClassificationBoards + { + get + { + ClassificationBoardItem boardItem = new ClassificationBoardItem{ + Board = AgeRatingCategory.ACB, + Classifications = new List{ + AgeRatingTitle.ACB_G, AgeRatingTitle.ACB_M, AgeRatingTitle.ACB_MA15, AgeRatingTitle.ACB_R18, AgeRatingTitle.ACB_RC + } + }; + + return new List{ + new ClassificationBoardItem{ + Board = AgeRatingCategory.ACB, + Classifications = new List{ + AgeRatingTitle.ACB_G, + AgeRatingTitle.ACB_M, + AgeRatingTitle.ACB_MA15, + AgeRatingTitle.ACB_R18, + AgeRatingTitle.ACB_RC + } + }, + new ClassificationBoardItem{ + Board = AgeRatingCategory.CERO, + Classifications = new List{ + AgeRatingTitle.CERO_A, + AgeRatingTitle.CERO_B, + AgeRatingTitle.CERO_C, + AgeRatingTitle.CERO_D, + AgeRatingTitle.CERO_Z + } + }, + new ClassificationBoardItem{ + Board = AgeRatingCategory.CLASS_IND, + Classifications = new List{ + AgeRatingTitle.CLASS_IND_L, + AgeRatingTitle.CLASS_IND_Ten, + AgeRatingTitle.CLASS_IND_Twelve, + AgeRatingTitle.CLASS_IND_Fourteen, + AgeRatingTitle.CLASS_IND_Sixteen, + AgeRatingTitle.CLASS_IND_Eighteen + } + } + }; + } + } + + readonly static AgeGroupItem Adult_Item = new AgeGroupItem{ + ACB = new List{ AgeRatingTitle.ACB_R18, AgeRatingTitle.ACB_RC }, + CERO = new List{ AgeRatingTitle.CERO_Z }, + CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Eighteen }, + ESRB = new List{ AgeRatingTitle.RP, AgeRatingTitle.AO }, + GRAC = new List{ AgeRatingTitle.GRAC_Eighteen }, + PEGI = new List{ AgeRatingTitle.Eighteen}, + USK = new List{ AgeRatingTitle.USK_18} + }; + + readonly static AgeGroupItem Mature_Item = new AgeGroupItem{ + ACB = new List{ AgeRatingTitle.ACB_M, AgeRatingTitle.ACB_MA15 }, + CERO = new List{ AgeRatingTitle.CERO_C, AgeRatingTitle.CERO_D }, + CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Sixteen }, + ESRB = new List{ AgeRatingTitle.M }, + GRAC = new List{ AgeRatingTitle.GRAC_Fifteen }, + PEGI = new List{ AgeRatingTitle.Sixteen}, + USK = new List{ AgeRatingTitle.USK_16} + }; + + readonly static AgeGroupItem Teen_Item = new AgeGroupItem{ + ACB = new List{ AgeRatingTitle.ACB_PG }, + CERO = new List{ AgeRatingTitle.CERO_B }, + CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_Twelve, AgeRatingTitle.CLASS_IND_Fourteen }, + ESRB = new List{ AgeRatingTitle.T }, + GRAC = new List{ AgeRatingTitle.GRAC_Twelve }, + PEGI = new List{ AgeRatingTitle.Twelve}, + USK = new List{ AgeRatingTitle.USK_12} + }; + + readonly static AgeGroupItem Child_Item = new AgeGroupItem{ + ACB = new List{ AgeRatingTitle.ACB_G }, + CERO = new List{ AgeRatingTitle.CERO_A }, + CLASS_IND = new List{ AgeRatingTitle.CLASS_IND_L, AgeRatingTitle.CLASS_IND_Ten }, + ESRB = new List{ AgeRatingTitle.E, AgeRatingTitle.E10 }, + GRAC = new List{ AgeRatingTitle.GRAC_All }, + PEGI = new List{ AgeRatingTitle.Three, AgeRatingTitle.Seven}, + USK = new List{ AgeRatingTitle.USK_0, AgeRatingTitle.USK_6} + }; + + public class AgeGroupItem + { + public List ACB { get; set; } + public List CERO { get; set; } + public List CLASS_IND { get; set; } + public List ESRB { get; set; } + public List GRAC { get; set; } + public List PEGI { get; set; } + public List USK { get; set; } + + [JsonIgnore] + [Newtonsoft.Json.JsonIgnore] + public List AgeGroupItemValues + { + get + { + List values = new List(); + { + foreach (AgeRatingTitle ageRatingTitle in ACB) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in CERO) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in CLASS_IND) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in ESRB) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in GRAC) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in PEGI) + { + values.Add((long)ageRatingTitle); + } + foreach (AgeRatingTitle ageRatingTitle in USK) + { + values.Add((long)ageRatingTitle); + } + } + + return values; + } + } + } + + public class ClassificationBoardItem + { + public IGDB.Models.AgeRatingCategory Board { get; set; } + public List Classifications { get; set; } + } + } } } diff --git a/gaseous-server/Controllers/V1.0/AccountController.cs b/gaseous-server/Controllers/V1.0/AccountController.cs new file mode 100644 index 0000000..9f072ae --- /dev/null +++ b/gaseous-server/Controllers/V1.0/AccountController.cs @@ -0,0 +1,396 @@ +using System.Security.Claims; +using System.Text; +using Authentication; +using gaseous_server.Classes; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] + public class AccountController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public AccountController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ILoggerFactory loggerFactory) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _logger = loggerFactory.CreateLogger(); + } + + [HttpPost] + [AllowAnonymous] + [Route("Login")] + public async Task Login(LoginViewModel model) + { + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + Logging.Log(Logging.LogType.Information, "Login", model.Email + " has logged in, from IP: " + HttpContext.Connection.RemoteIpAddress?.ToString()); + return Ok(result.ToString()); + } + // if (result.RequiresTwoFactor) + // { + // return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + // } + if (result.IsLockedOut) + { + Logging.Log(Logging.LogType.Warning, "Login", model.Email + " was unable to login due to a locked account. Login attempt from IP: " + HttpContext.Connection.RemoteIpAddress?.ToString()); + return Unauthorized(); + } + else + { + Logging.Log(Logging.LogType.Critical, "Login", "An unknown error occurred during login by " + model.Email + ". Login attempt from IP: " + HttpContext.Connection.RemoteIpAddress?.ToString()); + return Unauthorized(); + } + } + + // If we got this far, something failed, redisplay form + Logging.Log(Logging.LogType.Critical, "Login", "An unknown error occurred during login by " + model.Email + ". Login attempt from IP: " + HttpContext.Connection.RemoteIpAddress?.ToString()); + return Unauthorized(); + } + + [HttpPost] + [Route("LogOff")] + public async Task LogOff() + { + var userName = User.FindFirstValue(ClaimTypes.Name); + await _signInManager.SignOutAsync(); + if (userName != null) + { + Logging.Log(Logging.LogType.Information, "Login", userName + " has logged out"); + } + return Ok(); + } + + [HttpGet] + [Route("Profile/Basic")] + [Authorize] + public async Task ProfileBasic() + { + ProfileBasicViewModel profile = new ProfileBasicViewModel(); + profile.UserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + ApplicationUser user = await _userManager.FindByIdAsync(profile.UserId); + profile.UserName = _userManager.GetUserName(HttpContext.User); + profile.EmailAddress = await _userManager.GetEmailAsync(user); + profile.Roles = new List(await _userManager.GetRolesAsync(user)); + profile.SecurityProfile = user.SecurityProfile; + profile.Roles.Sort(); + + return Ok(profile); + } + + [HttpGet] + [Route("Profile/Basic/profile.js")] + [ApiExplorerSettings(IgnoreApi = true)] + [AllowAnonymous] + public async Task ProfileBasicFile() + { + var user = await _userManager.GetUserAsync(User); + if (user != null) + { + ProfileBasicViewModel profile = new ProfileBasicViewModel(); + profile.UserId = User.FindFirstValue(ClaimTypes.NameIdentifier); + profile.UserName = _userManager.GetUserName(HttpContext.User); + profile.EmailAddress = await _userManager.GetEmailAsync(user); + profile.Roles = new List(await _userManager.GetRolesAsync(user)); + profile.SecurityProfile = user.SecurityProfile; + profile.Roles.Sort(); + + string profileString = "var userProfile = " + Newtonsoft.Json.JsonConvert.SerializeObject(profile, Newtonsoft.Json.Formatting.Indented) + ";"; + + byte[] bytes = Encoding.UTF8.GetBytes(profileString); + return File(bytes, "text/javascript"); + } + else + { + string profileString = "var userProfile = null;"; + + byte[] bytes = Encoding.UTF8.GetBytes(profileString); + return File(bytes, "text/javascript"); + } + } + + [HttpPost] + [Route("ChangePassword")] + [Authorize] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await _userManager.GetUserAsync(User); + if (user == null) + { + return RedirectToAction("Login"); + } + + // ChangePasswordAsync changes the user password + var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + + // The new password did not meet the complexity rules or + // the current password is incorrect. Add these errors to + // the ModelState and rerender ChangePassword view + if (!result.Succeeded) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + return Unauthorized(result); + } + + // Upon successfully changing the password refresh sign-in cookie + await _signInManager.RefreshSignInAsync(user); + return Ok(); + } + + return NotFound(); + } + + [HttpGet] + [Route("Users")] + [Authorize(Roles = "Admin")] + public async Task GetAllUsers() + { + List users = new List(); + + foreach (ApplicationUser rawUser in _userManager.Users) + { + UserViewModel user = new UserViewModel(); + user.Id = rawUser.Id; + user.EmailAddress = rawUser.NormalizedEmail.ToLower(); + user.LockoutEnabled = rawUser.LockoutEnabled; + user.LockoutEnd = rawUser.LockoutEnd; + user.SecurityProfile = rawUser.SecurityProfile; + + // get roles + ApplicationUser? aUser = await _userManager.FindByIdAsync(rawUser.Id); + if (aUser != null) + { + IList aUserRoles = await _userManager.GetRolesAsync(aUser); + user.Roles = aUserRoles.ToList(); + + user.Roles.Sort(); + } + + users.Add(user); + } + + return Ok(users); + } + + [HttpPost] + [Route("Users")] + [Authorize(Roles = "Admin")] + public async Task NewUser(RegisterViewModel model) + { + if (ModelState.IsValid) + { + ApplicationUser user = new ApplicationUser + { + UserName = model.UserName, + NormalizedUserName = model.UserName.ToUpper(), + Email = model.Email, + NormalizedEmail = model.Email.ToUpper() + }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + // add new users to the player role + await _userManager.AddToRoleAsync(user, "Player"); + + Logging.Log(Logging.LogType.Information, "User Management", User.FindFirstValue(ClaimTypes.Name) + " created user " + model.Email + " with password."); + + return Ok(result); + } + else + { + return Ok(result); + } + } + else + { + return NotFound(); + } + } + + [HttpGet] + [Route("Users/{UserId}")] + [Authorize(Roles = "Admin")] + public async Task GetUser(string UserId) + { + ApplicationUser? rawUser = await _userManager.FindByIdAsync(UserId); + + if (rawUser != null) + { + UserViewModel user = new UserViewModel(); + user.Id = rawUser.Id; + user.EmailAddress = rawUser.NormalizedEmail.ToLower(); + user.LockoutEnabled = rawUser.LockoutEnabled; + user.LockoutEnd = rawUser.LockoutEnd; + user.SecurityProfile = rawUser.SecurityProfile; + + // get roles + IList aUserRoles = await _userManager.GetRolesAsync(rawUser); + user.Roles = aUserRoles.ToList(); + + user.Roles.Sort(); + + return Ok(user); + } + else + { + return NotFound(); + } + } + + [HttpDelete] + [Route("Users/{UserId}")] + [Authorize(Roles = "Admin")] + public async Task DeleteUser(string UserId) + { + // get user + ApplicationUser? user = await _userManager.FindByIdAsync(UserId); + + if (user == null) + { + return NotFound(); + } + else + { + await _userManager.DeleteAsync(user); + Logging.Log(Logging.LogType.Information, "User Management", User.FindFirstValue(ClaimTypes.Name) + " deleted user " + user.Email); + return Ok(); + } + } + + [HttpPost] + [Route("Users/{UserId}/Roles")] + [Authorize(Roles = "Admin")] + public async Task SetUserRoles(string UserId, string RoleName) + { + ApplicationUser? user = await _userManager.FindByIdAsync(UserId); + + if (user != null) + { + // get roles + List userRoles = (await _userManager.GetRolesAsync(user)).ToList(); + + // delete all roles + foreach (string role in userRoles) + { + if ((new string[] { "Admin", "Gamer", "Player" }).Contains(role) ) + { + await _userManager.RemoveFromRoleAsync(user, role); + } + } + + // add only requested roles + switch (RoleName) + { + case "Admin": + await _userManager.AddToRoleAsync(user, "Admin"); + await _userManager.AddToRoleAsync(user, "Gamer"); + await _userManager.AddToRoleAsync(user, "Player"); + break; + case "Gamer": + await _userManager.AddToRoleAsync(user, "Gamer"); + await _userManager.AddToRoleAsync(user, "Player"); + break; + case "Player": + await _userManager.AddToRoleAsync(user, "Player"); + break; + default: + await _userManager.AddToRoleAsync(user, RoleName); + break; + } + + return Ok(); + } + else + { + return NotFound(); + } + } + + [HttpPost] + [Route("Users/{UserId}/SecurityProfile")] + [Authorize(Roles = "Admin")] + public async Task SetUserSecurityProfile(string UserId, SecurityProfileViewModel securityProfile) + { + if (ModelState.IsValid) + { + ApplicationUser? user = await _userManager.FindByIdAsync(UserId); + + if (user != null) + { + user.SecurityProfile = securityProfile; + await _userManager.UpdateAsync(user); + + return Ok(); + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + + [HttpPost] + [Route("Users/{UserId}/Password")] + [Authorize(Roles = "Admin")] + public async Task ResetPassword(string UserId, SetPasswordViewModel model) + { + if (ModelState.IsValid) + { + // we can reset the users password + ApplicationUser? user = await _userManager.FindByIdAsync(UserId); + if (user != null) + { + string resetToken = await _userManager.GeneratePasswordResetTokenAsync(user); + IdentityResult passwordChangeResult = await _userManager.ResetPasswordAsync(user, resetToken, model.NewPassword); + if (passwordChangeResult.Succeeded == true) + { + return Ok(); + } + else + { + return Ok(passwordChangeResult); + } + } + else + { + return NotFound(); + } + } + else + { + return NotFound(); + } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/BackgroundTasksController.cs b/gaseous-server/Controllers/V1.0/BackgroundTasksController.cs similarity index 83% rename from gaseous-server/Controllers/BackgroundTasksController.cs rename to gaseous-server/Controllers/V1.0/BackgroundTasksController.cs index 3eec9a5..877bbd1 100644 --- a/gaseous-server/Controllers/BackgroundTasksController.cs +++ b/gaseous-server/Controllers/V1.0/BackgroundTasksController.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -9,9 +10,12 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize(Roles = "Admin,Gamer,Player")] public class BackgroundTasksController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List GetQueue() @@ -20,10 +24,12 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{TaskType}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Roles = "Admin")] public ActionResult ForceRun(ProcessQueue.QueueItemType TaskType, Boolean ForceRun) { foreach (ProcessQueue.QueueItem qi in ProcessQueue.QueueItems) diff --git a/gaseous-server/Controllers/BiosController.cs b/gaseous-server/Controllers/V1.0/BiosController.cs similarity index 93% rename from gaseous-server/Controllers/BiosController.cs rename to gaseous-server/Controllers/V1.0/BiosController.cs index caa0fbc..732b2f8 100644 --- a/gaseous-server/Controllers/BiosController.cs +++ b/gaseous-server/Controllers/V1.0/BiosController.cs @@ -4,6 +4,7 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using gaseous_server.Classes; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -11,9 +12,12 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] public class BiosController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List GetBios() @@ -22,6 +26,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{PlatformId}")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -31,8 +36,10 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpHead] [Route("zip/{PlatformId}")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -66,8 +73,10 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpHead] [Route("{PlatformId}/{BiosName}")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] diff --git a/gaseous-server/Controllers/CollectionsController.cs b/gaseous-server/Controllers/V1.0/CollectionsController.cs similarity index 93% rename from gaseous-server/Controllers/CollectionsController.cs rename to gaseous-server/Controllers/V1.0/CollectionsController.cs index 64c6ef3..c05a227 100644 --- a/gaseous-server/Controllers/CollectionsController.cs +++ b/gaseous-server/Controllers/V1.0/CollectionsController.cs @@ -4,6 +4,7 @@ using System.IO.Compression; using System.Linq; using System.Threading.Tasks; using gaseous_server.Classes; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -11,6 +12,8 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] public class CollectionsController : Controller { /// @@ -18,6 +21,7 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List GetCollections() @@ -32,6 +36,7 @@ namespace gaseous_server.Controllers /// Set to true to begin the collection build process /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{CollectionId}")] [ProducesResponseType(typeof(Classes.Collections.CollectionItem), StatusCodes.Status200OK)] @@ -59,6 +64,7 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{CollectionId}/Roms")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -82,8 +88,10 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] [Route("Preview")] + [Authorize(Roles = "Admin,Gamer")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult GetCollectionRomsPreview(Classes.Collections.CollectionItem Item) @@ -104,6 +112,7 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{CollectionId}/Roms/Zip")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -138,7 +147,9 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] + [Authorize(Roles = "Admin,Gamer")] [ProducesResponseType(typeof(Classes.Collections.CollectionItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public ActionResult NewCollection(Classes.Collections.CollectionItem Item) @@ -160,8 +171,10 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPatch] [Route("{CollectionId}")] + [Authorize(Roles = "Admin,Gamer")] [ProducesResponseType(typeof(Classes.Collections.CollectionItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public ActionResult EditCollection(long CollectionId, Classes.Collections.CollectionItem Item) @@ -183,7 +196,9 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPatch] + [Authorize(Roles = "Admin,Gamer")] [Route("{CollectionId}/AlwaysInclude")] [ProducesResponseType(typeof(Classes.Collections.CollectionItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -218,7 +233,9 @@ namespace gaseous_server.Controllers /// /// [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpDelete] + [Authorize(Roles = "Admin,Gamer")] [Route("{CollectionId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] diff --git a/gaseous-server/Controllers/FilterController.cs b/gaseous-server/Controllers/V1.0/FilterController.cs similarity index 97% rename from gaseous-server/Controllers/FilterController.cs rename to gaseous-server/Controllers/V1.0/FilterController.cs index cb90796..c83947a 100644 --- a/gaseous-server/Controllers/FilterController.cs +++ b/gaseous-server/Controllers/V1.0/FilterController.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading.Tasks; using gaseous_server.Classes; using IGDB.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; @@ -12,10 +13,13 @@ namespace gaseous_server.Controllers { [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] [ApiController] public class FilterController : ControllerBase { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] //[ResponseCache(CacheProfileName = "5Minute")] diff --git a/gaseous-server/Controllers/GamesController.cs b/gaseous-server/Controllers/V1.0/GamesController.cs similarity index 90% rename from gaseous-server/Controllers/GamesController.cs rename to gaseous-server/Controllers/V1.0/GamesController.cs index f7553cb..65085ef 100644 --- a/gaseous-server/Controllers/GamesController.cs +++ b/gaseous-server/Controllers/V1.0/GamesController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using gaseous_server.Classes; using gaseous_server.Classes.Metadata; using IGDB.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; @@ -18,6 +19,8 @@ namespace gaseous_server.Controllers { [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] [ApiController] public class GamesController : ControllerBase { @@ -36,7 +39,7 @@ namespace gaseous_server.Controllers bool sortdescending = false) { - return Ok(GetGames(name, platform, genre, gamemode, playerperspective, theme, minrating, maxrating, sortdescending)); + return Ok(GetGames(name, platform, genre, gamemode, playerperspective, theme, minrating, maxrating, "Adult", true, true, sortdescending)); } public static List GetGames( @@ -48,6 +51,9 @@ namespace gaseous_server.Controllers string theme = "", int minrating = -1, int maxrating = -1, + string ratinggroup = "Adult", + bool includenullrating = true, + bool sortbynamethe = false, bool sortdescending = false) { string whereClause = ""; @@ -170,6 +176,52 @@ namespace gaseous_server.Controllers whereClauses.Add(tempVal); } + if (ratinggroup.Length > 0) + { + List AgeClassificationsList = new List(); + foreach (string ratingGroup in ratinggroup.Split(',')) + { + if (AgeGroups.AgeGroupings.ContainsKey(ratingGroup)) + { + List ageGroups = AgeGroups.AgeGroupings[ratingGroup]; + foreach (AgeGroups.AgeGroupItem ageGroup in ageGroups) + { + AgeClassificationsList.AddRange(ageGroup.AgeGroupItemValues); + } + } + } + + if (AgeClassificationsList.Count > 0) + { + tempVal = "(view_AgeRatings.Rating IN ("; + for (int i = 0; i < AgeClassificationsList.Count; i++) + { + if (i > 0) + { + tempVal += ", "; + } + string themeLabel = "@Rating" + i; + tempVal += themeLabel; + whereParams.Add(themeLabel, AgeClassificationsList[i]); + } + tempVal += ")"; + + tempVal += " OR "; + + if (includenullrating == true) + { + tempVal += "view_AgeRatings.Rating IS NULL"; + } + else + { + tempVal += "view_AgeRatings.Rating IS NOT NULL"; + } + tempVal += ")"; + + whereClauses.Add(tempVal); + } + } + // build where clause if (whereClauses.Count > 0) { @@ -199,14 +251,21 @@ namespace gaseous_server.Controllers } // order by clause - string orderByClause = "ORDER BY `Name` ASC"; + string orderByField = "Name"; + if (sortbynamethe == true) + { + orderByField = "NameThe"; + } + string orderByClause = "ORDER BY `" + orderByField + "` ASC"; if (sortdescending == true) { - orderByClause = "ORDER BY `Name` DESC"; + orderByClause = "ORDER BY `" + orderByField + "` DESC"; } Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); - string sql = "SELECT DISTINCT Games_Roms.GameId AS ROMGameId, Game.* FROM Games_Roms LEFT JOIN Game ON Game.Id = Games_Roms.GameId LEFT JOIN Relation_Game_Genres ON Game.Id = Relation_Game_Genres.GameId LEFT JOIN Relation_Game_GameModes ON Game.Id = Relation_Game_GameModes.GameId LEFT JOIN Relation_Game_PlayerPerspectives ON Game.Id = Relation_Game_PlayerPerspectives.GameId LEFT JOIN Relation_Game_Themes ON Game.Id = Relation_Game_Themes.GameId " + whereClause + " " + havingClause + " " + orderByClause; + //string sql = "SELECT DISTINCT Games_Roms.GameId AS ROMGameId, Game.* FROM Games_Roms LEFT JOIN Game ON Game.Id = Games_Roms.GameId LEFT JOIN Relation_Game_Genres ON Game.Id = Relation_Game_Genres.GameId LEFT JOIN Relation_Game_GameModes ON Game.Id = Relation_Game_GameModes.GameId LEFT JOIN Relation_Game_PlayerPerspectives ON Game.Id = Relation_Game_PlayerPerspectives.GameId LEFT JOIN Relation_Game_Themes ON Game.Id = Relation_Game_Themes.GameId " + whereClause + " " + havingClause + " " + orderByClause; + + string sql = "SELECT DISTINCT Games_Roms.GameId AS ROMGameId, Game.*, case when Game.`Name` like 'The %' then CONCAT(trim(substr(Game.`Name` from 4)), ', The') else Game.`Name` end as NameThe FROM Games_Roms LEFT JOIN Game ON Game.Id = Games_Roms.GameId LEFT JOIN Relation_Game_Genres ON Game.Id = Relation_Game_Genres.GameId LEFT JOIN Relation_Game_GameModes ON Game.Id = Relation_Game_GameModes.GameId LEFT JOIN Relation_Game_PlayerPerspectives ON Game.Id = Relation_Game_PlayerPerspectives.GameId LEFT JOIN Relation_Game_Themes ON Game.Id = Relation_Game_Themes.GameId LEFT JOIN (SELECT Relation_Game_AgeRatings.GameId, AgeRating.* FROM Relation_Game_AgeRatings JOIN AgeRating ON Relation_Game_AgeRatings.AgeRatingsId = AgeRating.Id) view_AgeRatings ON Game.Id = view_AgeRatings.GameId " + whereClause + " " + havingClause + " " + orderByClause; List RetVal = new List(); @@ -221,6 +280,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}")] [ProducesResponseType(typeof(Game), StatusCodes.Status200OK)] @@ -248,6 +308,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/alternativename")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -280,6 +341,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/agerating")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -312,6 +374,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/agerating/{RatingId}/image")] [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] @@ -392,6 +455,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/artwork")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -422,6 +486,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/artwork/{ArtworkId}")] [ProducesResponseType(typeof(Artwork), StatusCodes.Status200OK)] @@ -457,6 +522,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/artwork/{ArtworkId}/image")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -512,6 +578,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/cover")] [ProducesResponseType(typeof(Cover), StatusCodes.Status200OK)] @@ -546,6 +613,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/cover/image")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -586,6 +654,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/genre")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -623,6 +692,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/companies")] [ProducesResponseType(typeof(List>), StatusCodes.Status200OK)] @@ -667,6 +737,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/companies/{CompanyId}")] [ProducesResponseType(typeof(Dictionary), StatusCodes.Status200OK)] @@ -709,6 +780,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/companies/{CompanyId}/image")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -753,6 +825,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/roms")] [ProducesResponseType(typeof(Classes.Roms.GameRomObject), StatusCodes.Status200OK)] @@ -773,6 +846,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/roms/{RomId}")] [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] @@ -801,7 +875,9 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPatch] + [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/roms/{RomId}")] [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -829,7 +905,9 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpDelete] + [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/roms/{RomId}")] [ProducesResponseType(typeof(Classes.Roms.GameRomItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -857,8 +935,10 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpHead] [Route("{GameId}/roms/{RomId}/file")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -894,8 +974,10 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpHead] [Route("{GameId}/roms/{RomId}/{FileName}")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -931,6 +1013,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/romgroup/{RomGroupId}")] [ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)] @@ -959,7 +1042,9 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] + [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/romgroup")] [ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -986,7 +1071,9 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPatch] + [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/romgroup/{RomId}")] [ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -1014,7 +1101,9 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpDelete] + [Authorize(Roles = "Admin,Gamer")] [Route("{GameId}/romgroup/{RomGroupId}")] [ProducesResponseType(typeof(Classes.RomMediaGroup.GameRomMediaGroupItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -1042,8 +1131,10 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpHead] [Route("{GameId}/romgroup/{RomGroupId}/file")] [Route("{GameId}/romgroup/{RomGroupId}/{filename}")] @@ -1089,6 +1180,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("search")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -1127,6 +1219,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/screenshots")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -1157,6 +1250,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/screenshots/{ScreenshotId}")] [ProducesResponseType(typeof(Screenshot), StatusCodes.Status200OK)] @@ -1190,6 +1284,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/screenshots/{ScreenshotId}/image")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] @@ -1233,6 +1328,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{GameId}/videos")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] diff --git a/gaseous-server/Controllers/LibraryController.cs b/gaseous-server/Controllers/V1.0/LibraryController.cs similarity index 91% rename from gaseous-server/Controllers/LibraryController.cs rename to gaseous-server/Controllers/V1.0/LibraryController.cs index 37c44c2..fe5981e 100644 --- a/gaseous-server/Controllers/LibraryController.cs +++ b/gaseous-server/Controllers/V1.0/LibraryController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO.Compression; using System.Linq; using System.Threading.Tasks; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -10,9 +11,12 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize(Roles = "Admin")] public class LibraryController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public ActionResult GetLibraries() @@ -21,6 +25,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet("{LibraryId}")] [ProducesResponseType(typeof(GameLibrary.LibraryItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -37,6 +42,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] [ProducesResponseType(typeof(GameLibrary.LibraryItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] @@ -58,6 +64,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpDelete("{LibraryId}")] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] diff --git a/gaseous-server/Controllers/LogsController.cs b/gaseous-server/Controllers/V1.0/LogsController.cs similarity index 83% rename from gaseous-server/Controllers/LogsController.cs rename to gaseous-server/Controllers/V1.0/LogsController.cs index 8b536db..e9e00a9 100644 --- a/gaseous-server/Controllers/LogsController.cs +++ b/gaseous-server/Controllers/V1.0/LogsController.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using gaseous_server.Classes; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -10,9 +11,12 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize(Roles = "Admin")] public class LogsController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List Logs(long? StartIndex, int PageNumber = 1, int PageSize = 100) diff --git a/gaseous-server/Controllers/PlatformMapsController.cs b/gaseous-server/Controllers/V1.0/PlatformMapsController.cs similarity index 94% rename from gaseous-server/Controllers/PlatformMapsController.cs rename to gaseous-server/Controllers/V1.0/PlatformMapsController.cs index f124e65..7d5f055 100644 --- a/gaseous-server/Controllers/PlatformMapsController.cs +++ b/gaseous-server/Controllers/V1.0/PlatformMapsController.cs @@ -12,15 +12,19 @@ using IGDB.Models; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; +using Microsoft.AspNetCore.Authorization; namespace gaseous_server.Controllers { [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] [ApiController] + [Authorize] public class PlatformMapsController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public ActionResult GetPlatformMap(bool ResetToDefault = false) @@ -34,6 +38,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{PlatformId}")] [ProducesResponseType(typeof(PlatformMapping.PlatformMapItem), StatusCodes.Status200OK)] @@ -60,10 +65,12 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [RequestSizeLimit(long.MaxValue)] [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] + [Authorize(Roles = "Admin")] public async Task UploadPlatformMap(List files) { Guid sessionid = Guid.NewGuid(); @@ -115,6 +122,7 @@ namespace gaseous_server.Controllers } // [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] // [Route("{PlatformId}")] // [ProducesResponseType(typeof(PlatformMapping.PlatformMapItem), StatusCodes.Status200OK)] @@ -143,10 +151,12 @@ namespace gaseous_server.Controllers // } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPatch] [Route("{PlatformId}")] [ProducesResponseType(typeof(PlatformMapping.PlatformMapItem), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] + [Authorize(Roles = "Admin")] public ActionResult EditPlatformMap(long PlatformId, PlatformMapping.PlatformMapItem Map) { try diff --git a/gaseous-server/Controllers/PlatformsController.cs b/gaseous-server/Controllers/V1.0/PlatformsController.cs similarity index 95% rename from gaseous-server/Controllers/PlatformsController.cs rename to gaseous-server/Controllers/V1.0/PlatformsController.cs index 9cbed4f..2cd370e 100644 --- a/gaseous-server/Controllers/PlatformsController.cs +++ b/gaseous-server/Controllers/V1.0/PlatformsController.cs @@ -9,6 +9,7 @@ using gaseous_server.Classes; using gaseous_server.Classes.Metadata; using gaseous_server.Models; using IGDB.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; @@ -17,10 +18,13 @@ namespace gaseous_server.Controllers { [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] [ApiController] public class PlatformsController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] public ActionResult Platform() @@ -46,6 +50,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{PlatformId}")] [ProducesResponseType(typeof(Platform), StatusCodes.Status200OK)] @@ -72,6 +77,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{PlatformId}/platformlogo")] [ProducesResponseType(typeof(PlatformLogo), StatusCodes.Status200OK)] @@ -105,6 +111,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("{PlatformId}/platformlogo/image")] [ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)] diff --git a/gaseous-server/Controllers/RomsController.cs b/gaseous-server/Controllers/V1.0/RomsController.cs similarity index 95% rename from gaseous-server/Controllers/RomsController.cs rename to gaseous-server/Controllers/V1.0/RomsController.cs index a1d2574..e2129d8 100644 --- a/gaseous-server/Controllers/RomsController.cs +++ b/gaseous-server/Controllers/V1.0/RomsController.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using gaseous_server.Classes; using gaseous_server.Classes.Metadata; using IGDB.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.CodeAnalysis.Scripting; @@ -18,11 +19,15 @@ namespace gaseous_server.Controllers { [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] [ApiController] public class RomsController : ControllerBase { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpPost] + [Authorize(Roles = "Admin,Gamer")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] [RequestSizeLimit(long.MaxValue)] [DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)] diff --git a/gaseous-server/Controllers/SearchController.cs b/gaseous-server/Controllers/V1.0/SearchController.cs similarity index 94% rename from gaseous-server/Controllers/SearchController.cs rename to gaseous-server/Controllers/V1.0/SearchController.cs index d221295..2321fa6 100644 --- a/gaseous-server/Controllers/SearchController.cs +++ b/gaseous-server/Controllers/V1.0/SearchController.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using gaseous_server.Classes; using IGDB; using IGDB.Models; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using static gaseous_server.Classes.Metadata.Games; @@ -14,6 +15,8 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] public class SearchController : Controller { private static IGDBClient igdb = new IGDBClient( @@ -23,6 +26,7 @@ namespace gaseous_server.Controllers ); [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("Platform")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] @@ -45,6 +49,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("Game")] [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] diff --git a/gaseous-server/Controllers/SignaturesController.cs b/gaseous-server/Controllers/V1.0/SignaturesController.cs similarity index 96% rename from gaseous-server/Controllers/SignaturesController.cs rename to gaseous-server/Controllers/V1.0/SignaturesController.cs index d9d72f6..5eb0e07 100644 --- a/gaseous-server/Controllers/SignaturesController.cs +++ b/gaseous-server/Controllers/V1.0/SignaturesController.cs @@ -6,6 +6,7 @@ using System.Security.Cryptography; using System.Threading.Tasks; using gaseous_server.Classes; using gaseous_signature_parser.models.RomSignatureObject; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; // For more information on enabling MVC for empty projects, visit https://go.microsoft.com/fwlink/?LinkID=397860 @@ -15,6 +16,8 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]/[action]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] public class SignaturesController : ControllerBase { /// @@ -22,6 +25,7 @@ namespace gaseous_server.Controllers /// /// Number of sources, publishers, games, and rom signatures in the database [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public Models.Signatures_Status Status() @@ -30,6 +34,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List GetSignature(string md5 = "", string sha1 = "") @@ -44,6 +49,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public List GetByTosecName(string TosecName = "") diff --git a/gaseous-server/Controllers/SystemController.cs b/gaseous-server/Controllers/V1.0/SystemController.cs similarity index 80% rename from gaseous-server/Controllers/SystemController.cs rename to gaseous-server/Controllers/V1.0/SystemController.cs index 768827d..cba567b 100644 --- a/gaseous-server/Controllers/SystemController.cs +++ b/gaseous-server/Controllers/V1.0/SystemController.cs @@ -4,8 +4,11 @@ using System.Data; using System.Linq; using System.Reflection; using System.Text; +using System.Text.Json; using System.Threading.Tasks; using gaseous_server.Classes; +using gaseous_server.Classes.Metadata; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; namespace gaseous_server.Controllers @@ -13,9 +16,12 @@ namespace gaseous_server.Controllers [ApiController] [Route("api/v{version:apiVersion}/[controller]")] [ApiVersion("1.0")] + [ApiVersion("1.1")] + [Authorize] public class SystemController : Controller { [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [ProducesResponseType(StatusCodes.Status200OK)] public SystemInfo GetSystemStatus() @@ -56,6 +62,7 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("Version")] [ProducesResponseType(StatusCodes.Status200OK)] @@ -64,13 +71,30 @@ namespace gaseous_server.Controllers } [MapToApiVersion("1.0")] + [MapToApiVersion("1.1")] [HttpGet] [Route("VersionFile")] + [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] public FileContentResult GetSystemVersionAsFile() { Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + + // get age ratings dictionary + Dictionary AgeRatingsStrings = new Dictionary(); + foreach(IGDB.Models.AgeRatingTitle ageRatingTitle in Enum.GetValues(typeof(IGDB.Models.AgeRatingTitle)) ) + { + AgeRatingsStrings.Add((int)ageRatingTitle, ageRatingTitle.ToString()); + } + string ver = "var AppVersion = \"" + Assembly.GetExecutingAssembly().GetName().Version.ToString() + "\";" + Environment.NewLine + - "var DBSchemaVersion = \"" + db.GetDatabaseSchemaVersion() + "\";"; + "var DBSchemaVersion = \"" + db.GetDatabaseSchemaVersion() + "\";" + Environment.NewLine + + "var FirstRunStatus = " + Config.ReadSetting("FirstRunStatus", "0") + ";" + Environment.NewLine + + "var AgeRatingStrings = " + JsonSerializer.Serialize(AgeRatingsStrings, new JsonSerializerOptions{ + WriteIndented = true + }) + ";" + Environment.NewLine + + "var AgeRatingGroups = " + JsonSerializer.Serialize(AgeRatings.AgeGroups.AgeGroupingsFlat, new JsonSerializerOptions{ + WriteIndented = true + }) + ";"; byte[] bytes = Encoding.UTF8.GetBytes(ver); return File(bytes, "text/javascript"); } diff --git a/gaseous-server/Controllers/V1.1/FirstSetupController.cs b/gaseous-server/Controllers/V1.1/FirstSetupController.cs new file mode 100644 index 0000000..fa5f67b --- /dev/null +++ b/gaseous-server/Controllers/V1.1/FirstSetupController.cs @@ -0,0 +1,78 @@ +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Authentication; +using gaseous_server.Classes; +using gaseous_server.Classes.Metadata; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.1")] + [Authorize] + public class FirstSetupController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public FirstSetupController( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [MapToApiVersion("1.1")] + [HttpPost] + [Route("0")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public async Task CreateAdminAccount(Authentication.RegisterViewModel model) + { + if (Config.ReadSetting("FirstRunStatus", "0") == "0") + { + if (ModelState.IsValid) + { + ApplicationUser user = new ApplicationUser + { + UserName = model.UserName, + NormalizedUserName = model.UserName.ToUpper(), + Email = model.Email, + NormalizedEmail = model.Email.ToUpper(), + SecurityProfile = new SecurityProfileViewModel() + }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + await _userManager.AddToRoleAsync(user, "Player"); + await _userManager.AddToRoleAsync(user, "Gamer"); + await _userManager.AddToRoleAsync(user, "Admin"); + + await _signInManager.SignInAsync(user, isPersistent: true); + + Config.SetSetting("FirstRunStatus", "1"); + + return Ok(); + } + } + + return NotFound(); + } + else + { + return NotFound(); + } + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/V1.1/GamesController.cs b/gaseous-server/Controllers/V1.1/GamesController.cs new file mode 100644 index 0000000..73e61df --- /dev/null +++ b/gaseous-server/Controllers/V1.1/GamesController.cs @@ -0,0 +1,413 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using Authentication; +using gaseous_server.Classes; +using gaseous_server.Classes.Metadata; +using IGDB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Scripting; +using static gaseous_server.Classes.Metadata.AgeRatings; + +namespace gaseous_server.Controllers.v1_1 +{ + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.1")] + [ApiController] + [Authorize] + public class GamesController: ControllerBase + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + + public GamesController( + UserManager userManager, + SignInManager signInManager) + { + _userManager = userManager; + _signInManager = signInManager; + } + + [MapToApiVersion("1.1")] + [HttpPost] + [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] + public async Task Game_v1_1(GameSearchModel model) + { + var user = await _userManager.GetUserAsync(User); + + if (user != null) + { + // apply security profile filtering + List RemoveAgeGroups = new List(); + switch (user.SecurityProfile.AgeRestrictionPolicy.MaximumAgeRestriction.ToLower()) + { + case "adult": + break; + case "mature": + RemoveAgeGroups.Add("Adult"); + break; + case "teen": + RemoveAgeGroups.Add("Adult"); + RemoveAgeGroups.Add("Mature"); + break; + case "child": + RemoveAgeGroups.Add("Adult"); + RemoveAgeGroups.Add("Mature"); + RemoveAgeGroups.Add("Teen"); + break; + } + foreach (string RemoveAgeGroup in RemoveAgeGroups) + { + if (model.GameAgeRating.AgeGroupings.Contains(RemoveAgeGroup)) + { + model.GameAgeRating.AgeGroupings.Remove(RemoveAgeGroup); + } + } + if (user.SecurityProfile.AgeRestrictionPolicy.IncludeUnrated == false) + { + model.GameAgeRating.IncludeUnrated = false; + } + + return Ok(GetGames(model)); + } + else + { + return Unauthorized(); + } + } + + public class GameSearchModel + { + public string Name { get; set; } + public List Platform { get; set; } + public List Genre { get; set; } + public List GameMode { get; set; } + public List PlayerPerspective { get; set; } + public List Theme { get; set; } + public GameRatingItem GameRating { get; set; } = new GameRatingItem(); + public GameAgeRatingItem GameAgeRating { get; set; } = new GameAgeRatingItem(); + public GameSortingItem Sorting { get; set; } = new GameSortingItem(); + + + public class GameRatingItem + { + public int MinimumRating { get; set; } = -1; + public int MinimumRatingCount { get; set; } = -1; + public int MaximumRating { get; set; } = -1; + public int MaximumRatingCount { get; set; } = -1; + public bool IncludeUnrated { get; set; } = false; + } + + public class GameAgeRatingItem + { + public List AgeGroupings { get; set; } = new List{ "Child", "Teen", "Mature", "Adult" }; + public bool IncludeUnrated { get; set; } = true; + } + + public class GameSortingItem + { + public SortField SortBy { get; set; } = SortField.NameThe; + public bool SortAscending { get; set; } = true; + + public enum SortField + { + Name, + NameThe, + Rating, + RatingCount + } + } + } + + public static List GetGames(GameSearchModel model) + { + string whereClause = ""; + string havingClause = ""; + Dictionary whereParams = new Dictionary(); + + List whereClauses = new List(); + List havingClauses = new List(); + + string tempVal = ""; + + if (model.Name.Length > 0) + { + tempVal = "`Name` LIKE @Name"; + whereParams.Add("@Name", "%" + model.Name + "%"); + havingClauses.Add(tempVal); + } + + if (model.GameRating != null) + { + List ratingClauses = new List(); + if (model.GameRating.MinimumRating != -1) + { + string ratingTempMinVal = "totalRating >= @totalMinRating"; + whereParams.Add("@totalMinRating", model.GameRating.MinimumRating); + ratingClauses.Add(ratingTempMinVal); + } + + if (model.GameRating.MaximumRating != -1) + { + string ratingTempMaxVal = "totalRating <= @totalMaxRating"; + whereParams.Add("@totalMaxRating", model.GameRating.MaximumRating); + ratingClauses.Add(ratingTempMaxVal); + } + + if (model.GameRating.MinimumRatingCount != -1) + { + string ratingTempMinCountVal = "totalRatingCount >= @totalMinRatingCount"; + whereParams.Add("@totalMinRatingCount", model.GameRating.MinimumRatingCount); + ratingClauses.Add(ratingTempMinCountVal); + } + + if (model.GameRating.MaximumRatingCount != -1) + { + string ratingTempMaxCountVal = "totalRatingCount <= @totalMaxRatingCount"; + whereParams.Add("@totalMaxRatingCount", model.GameRating.MaximumRatingCount); + ratingClauses.Add(ratingTempMaxCountVal); + } + + // generate rating sub clause + string ratingClauseValue = ""; + if (ratingClauses.Count > 0) + { + foreach (string ratingClause in ratingClauses) + { + if (ratingClauseValue.Length > 0) + { + ratingClauseValue += " AND "; + } + ratingClauseValue += ratingClause; + } + } + + string unratedClause = "totalRating IS NOT NULL"; + if (model.GameRating.IncludeUnrated == true) + { + unratedClause = "totalRating IS NULL"; + } + + if (ratingClauseValue.Length > 0) + { + havingClauses.Add("((" + ratingClauseValue + ") OR " + unratedClause + ")"); + } + } + + if (model.Platform.Count > 0) + { + tempVal = "Games_Roms.PlatformId IN ("; + for (int i = 0; i < model.Platform.Count; i++) + { + if (i > 0) + { + tempVal += ", "; + } + string platformLabel = "@Platform" + i; + tempVal += platformLabel; + whereParams.Add(platformLabel, model.Platform[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + if (model.Genre.Count > 0) + { + tempVal = "Relation_Game_Genres.GenresId IN ("; + for (int i = 0; i < model.Genre.Count; i++) + { + if (i > 0) + { + tempVal += " AND "; + } + string genreLabel = "@Genre" + i; + tempVal += genreLabel; + whereParams.Add(genreLabel, model.Genre[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + if (model.GameMode.Count > 0) + { + tempVal = "Relation_Game_GameModes.GameModesId IN ("; + for (int i = 0; i < model.GameMode.Count; i++) + { + if (i > 0) + { + tempVal += " AND "; + } + string gameModeLabel = "@GameMode" + i; + tempVal += gameModeLabel; + whereParams.Add(gameModeLabel, model.GameMode[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + if (model.PlayerPerspective.Count > 0) + { + tempVal = "Relation_Game_PlayerPerspectives.PlayerPerspectivesId IN ("; + for (int i = 0; i < model.PlayerPerspective.Count; i++) + { + if (i > 0) + { + tempVal += " AND "; + } + string playerPerspectiveLabel = "@PlayerPerspective" + i; + tempVal += playerPerspectiveLabel; + whereParams.Add(playerPerspectiveLabel, model.PlayerPerspective[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + if (model.Theme.Count > 0) + { + tempVal = "Relation_Game_Themes.ThemesId IN ("; + for (int i = 0; i < model.Theme.Count; i++) + { + if (i > 0) + { + tempVal += " AND "; + } + string themeLabel = "@Theme" + i; + tempVal += themeLabel; + whereParams.Add(themeLabel, model.Theme[i]); + } + tempVal += ")"; + whereClauses.Add(tempVal); + } + + if (model.GameAgeRating != null) + { + if (model.GameAgeRating.AgeGroupings.Count > 0) + { + List AgeClassificationsList = new List(); + foreach (string ratingGroup in model.GameAgeRating.AgeGroupings) + { + if (AgeGroups.AgeGroupings.ContainsKey(ratingGroup)) + { + List ageGroups = AgeGroups.AgeGroupings[ratingGroup]; + foreach (AgeGroups.AgeGroupItem ageGroup in ageGroups) + { + AgeClassificationsList.AddRange(ageGroup.AgeGroupItemValues); + } + } + } + + if (AgeClassificationsList.Count > 0) + { + AgeClassificationsList = new HashSet(AgeClassificationsList).ToList(); + tempVal = "(view_AgeRatings.Rating IN ("; + for (int i = 0; i < AgeClassificationsList.Count; i++) + { + if (i > 0) + { + tempVal += ", "; + } + string themeLabel = "@Rating" + i; + tempVal += themeLabel; + whereParams.Add(themeLabel, AgeClassificationsList[i]); + } + tempVal += ")"; + + if (model.GameAgeRating.IncludeUnrated == true) + { + tempVal += " OR view_AgeRatings.Rating IS NULL"; + } + tempVal += ")"; + + whereClauses.Add(tempVal); + } + } + } + + // build where clause + if (whereClauses.Count > 0) + { + whereClause = "WHERE "; + for (int i = 0; i < whereClauses.Count; i++) + { + if (i > 0) + { + whereClause += " AND "; + } + whereClause += whereClauses[i]; + } + } + + // build having clause + if (havingClauses.Count > 0) + { + havingClause = "HAVING "; + for (int i = 0; i < havingClauses.Count; i++) + { + if (i > 0) + { + havingClause += " AND "; + } + havingClause += havingClauses[i]; + } + } + + // order by clause + string orderByField = "NameThe"; + string orderByOrder = "ASC"; + if (model.Sorting != null) + { + switch(model.Sorting.SortBy) + { + case GameSearchModel.GameSortingItem.SortField.NameThe: + orderByField = "NameThe"; + break; + case GameSearchModel.GameSortingItem.SortField.Name: + orderByField = "Name"; + break; + case GameSearchModel.GameSortingItem.SortField.Rating: + orderByField = "TotalRating"; + break; + case GameSearchModel.GameSortingItem.SortField.RatingCount: + orderByField = "TotalRatingCount"; + break; + default: + orderByField = "NameThe"; + break; + } + + if (model.Sorting.SortAscending == true) + { + orderByOrder = "ASC"; + } + else + { + orderByOrder = "DESC"; + } + } + string orderByClause = "ORDER BY `" + orderByField + "` " + orderByOrder; + + Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString); + string sql = "SELECT DISTINCT Games_Roms.GameId AS ROMGameId, Game.*, case when Game.`Name` like 'The %' then CONCAT(trim(substr(Game.`Name` from 4)), ', The') else Game.`Name` end as NameThe FROM Games_Roms LEFT JOIN Game ON Game.Id = Games_Roms.GameId LEFT JOIN Relation_Game_Genres ON Game.Id = Relation_Game_Genres.GameId LEFT JOIN Relation_Game_GameModes ON Game.Id = Relation_Game_GameModes.GameId LEFT JOIN Relation_Game_PlayerPerspectives ON Game.Id = Relation_Game_PlayerPerspectives.GameId LEFT JOIN Relation_Game_Themes ON Game.Id = Relation_Game_Themes.GameId LEFT JOIN (SELECT Relation_Game_AgeRatings.GameId, AgeRating.* FROM Relation_Game_AgeRatings JOIN AgeRating ON Relation_Game_AgeRatings.AgeRatingsId = AgeRating.Id) view_AgeRatings ON Game.Id = view_AgeRatings.GameId " + whereClause + " " + havingClause + " " + orderByClause; + + List RetVal = new List(); + + DataTable dbResponse = db.ExecuteCMD(sql, whereParams); + foreach (DataRow dr in dbResponse.Rows) + { + //RetVal.Add(Classes.Metadata.Games.GetGame((long)dr["ROMGameId"], false, false)); + RetVal.Add(Classes.Metadata.Games.GetGame(dr)); + } + + return RetVal; + } + } +} \ No newline at end of file diff --git a/gaseous-server/Controllers/V1.1/RatingsController.cs b/gaseous-server/Controllers/V1.1/RatingsController.cs new file mode 100644 index 0000000..d02355d --- /dev/null +++ b/gaseous-server/Controllers/V1.1/RatingsController.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Data; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading.Tasks; +using gaseous_server.Classes; +using gaseous_server.Classes.Metadata; +using IGDB.Models; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.CodeAnalysis.Scripting; +using static gaseous_server.Classes.Metadata.AgeRatings; + +namespace gaseous_server.Controllers.v1_1 +{ + [Route("api/v{version:apiVersion}/[controller]")] + [ApiVersion("1.1")] + [ApiController] + public class RatingsController: ControllerBase + { + [MapToApiVersion("1.1")] + [HttpGet] + [Authorize] + [Route("Images/{RatingBoard}/{RatingId}/image.svg")] + [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status404NotFound)] + public ActionResult RatingsImageById(string RatingBoard, int RatingId) + { + IGDB.Models.AgeRatingTitle RatingTitle = (AgeRatingTitle)RatingId; + + string resourceName = "gaseous_server.Assets.Ratings." + RatingBoard + "." + RatingTitle.ToString() + ".svg"; + + var assembly = Assembly.GetExecutingAssembly(); + string[] resources = assembly.GetManifestResourceNames(); + if (resources.Contains(resourceName)) + { + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) + using (StreamReader reader = new StreamReader(stream)) + { + byte[] filedata = new byte[stream.Length]; + stream.Read(filedata, 0, filedata.Length); + + string filename = RatingBoard + "-" + RatingTitle.ToString() + ".svg"; + string contentType = "image/svg+xml"; + + 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); + } + } + return NotFound(); + } + } +} \ No newline at end of file diff --git a/gaseous-server/Program.cs b/gaseous-server/Program.cs index e1b7aa1..cd69181 100644 --- a/gaseous-server/Program.cs +++ b/gaseous-server/Program.cs @@ -7,8 +7,12 @@ using gaseous_server.SignatureIngestors.XML; using Microsoft.AspNetCore.Http.Features; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Versioning; +using Microsoft.Extensions.DependencyInjection; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.OpenApi.Models; +using Authentication; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; Logging.WriteToDiskOnly = true; Logging.Log(Logging.LogType.Information, "Startup", "Starting Gaseous Server " + Assembly.GetExecutingAssembly().GetName().Version); @@ -117,11 +121,14 @@ builder.Services.AddApiVersioning(config => config.DefaultApiVersion = new ApiVersion(1, 0); config.AssumeDefaultVersionWhenUnspecified = true; config.ReportApiVersions = true; + config.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(), + new HeaderApiVersionReader("x-api-version"), + new MediaTypeApiVersionReader("x-api-version")); }); -builder.Services.AddApiVersioning(setup => -{ - setup.ApiVersionReader = new UrlSegmentApiVersionReader(); -}); +// builder.Services.AddApiVersioning(setup => +// { +// setup.ApiVersionReader = new UrlSegmentApiVersionReader(); +// }); builder.Services.AddVersionedApiExplorer(setup => { setup.GroupNameFormat = "'v'VVV"; @@ -166,6 +173,24 @@ builder.Services.AddSwaggerGen(options => } }); + options.SwaggerDoc("v1.1", new OpenApiInfo + { + Version = "v1.1", + Title = "Gaseous Server API", + Description = "An API for managing the Gaseous Server", + TermsOfService = new Uri("https://github.com/gaseous-project/gaseous-server"), + Contact = new OpenApiContact + { + Name = "GitHub Repository", + Url = new Uri("https://github.com/gaseous-project/gaseous-server") + }, + License = new OpenApiLicense + { + Name = "Gaseous Server License", + Url = new Uri("https://github.com/gaseous-project/gaseous-server/blob/main/LICENSE") + } + }); + // using System.Reflection; var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; options.IncludeXmlComments(Path.Combine(AppContext.BaseDirectory, xmlFilename)); @@ -173,19 +198,115 @@ builder.Services.AddSwaggerGen(options => ); builder.Services.AddHostedService(); +// identity +builder.Services.AddIdentity(options => + { + options.Password.RequireDigit = true; + options.Password.RequireLowercase = true; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequireUppercase = true; + options.Password.RequiredLength = 10; + options.User.AllowedUserNameCharacters = null; + options.User.RequireUniqueEmail = true; + options.SignIn.RequireConfirmedPhoneNumber = false; + options.SignIn.RequireConfirmedEmail = false; + options.SignIn.RequireConfirmedAccount = false; + }) + .AddUserStore() + .AddRoleStore() + .AddDefaultTokenProviders() + .AddDefaultUI() + ; +builder.Services.ConfigureApplicationCookie(options => + { + options.Cookie.Name = "Gaseous.Identity"; + options.ExpireTimeSpan = TimeSpan.FromDays(90); + options.SlidingExpiration = true; + }); +// builder.Services.AddIdentityCore(options => { +// options.SignIn.RequireConfirmedAccount = false; +// options.User.RequireUniqueEmail = true; +// options.Password.RequireDigit = false; +// options.Password.RequiredLength = 10; +// options.Password.RequireNonAlphanumeric = false; +// options.Password.RequireUppercase = false; +// options.Password.RequireLowercase = false; +// }); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddTransient, UserStore>(); +builder.Services.AddTransient, RoleStore>(); + +builder.Services.AddAuthorization(options => +{ + options.AddPolicy("Admin", policy => policy.RequireRole("Admin")); + options.AddPolicy("Gamer", policy => policy.RequireRole("Gamer")); + options.AddPolicy("Player", policy => policy.RequireRole("Player")); +}); + +// builder.Services.AddControllersWithViews(options => +// { +// options.Filters.Add(new Microsoft.AspNetCore.Mvc.ValidateAntiForgeryTokenAttribute()); +// }); + var app = builder.Build(); // Configure the HTTP request pipeline. //if (app.Environment.IsDevelopment()) //{ app.UseSwagger(); -app.UseSwaggerUI(); +app.UseSwaggerUI(options => + { + options.SwaggerEndpoint($"/swagger/v1/swagger.json", "v1.0"); + options.SwaggerEndpoint($"/swagger/v1.1/swagger.json", "v1.1"); + } +); //} //app.UseHttpsRedirection(); app.UseResponseCaching(); +// set up system roles +using (var scope = app.Services.CreateScope()) +{ + var roleManager = scope.ServiceProvider.GetRequiredService(); + var roles = new[] { "Admin", "Gamer", "Player" }; + + foreach (var role in roles) + { + if (await roleManager.FindByNameAsync(role, CancellationToken.None) == null) + { + ApplicationRole applicationRole = new ApplicationRole(); + applicationRole.Name = role; + applicationRole.NormalizedName = role.ToUpper(); + await roleManager.CreateAsync(applicationRole, CancellationToken.None); + } + } + + // set up administrator account + var userManager = scope.ServiceProvider.GetRequiredService(); + if (await userManager.FindByNameAsync("admin@localhost", CancellationToken.None) == null) + { + ApplicationUser adminUser = new ApplicationUser{ + Id = Guid.NewGuid().ToString(), + Email = "admin@localhost", + NormalizedEmail = "ADMIN@LOCALHOST", + EmailConfirmed = true, + UserName = "administrator", + NormalizedUserName = "ADMINISTRATOR" + }; + + //set user password + PasswordHasher ph = new PasswordHasher(); + adminUser.PasswordHash = ph.HashPassword(adminUser, "letmein"); + + await userManager.CreateAsync(adminUser, CancellationToken.None); + await userManager.AddToRoleAsync(adminUser, "Admin", CancellationToken.None); + } +} + app.UseAuthorization(); app.UseDefaultFiles(); @@ -197,6 +318,72 @@ app.UseStaticFiles(new StaticFileOptions app.MapControllers(); +// emergency password recovery if environment variable is set +// process: +// - set the environment variable "recoveraccount" to the email address of the account to be recovered +// - when the server starts the password will be reset to a random string and saved in the library +// directory with the name RecoverAccount.txt +// - user should copy this password and remove the "recoveraccount" environment variable and the +// RecoverAccount.txt file +// - the server will not start while the RecoverAccount.txt file exists +string PasswordRecoveryFile = Path.Combine(Config.LibraryConfiguration.LibraryRootDirectory, "RecoverAccount.txt"); +if (!String.IsNullOrEmpty(Environment.GetEnvironmentVariable("recoveraccount"))) +{ + if (File.Exists(PasswordRecoveryFile)) + { + // password has already been set - do nothing and just exit + Logging.Log(Logging.LogType.Critical, "Server Startup", "Unable to start while recoveraccount environment varibale is set and RecoverAccount.txt file exists.", null, true); + Environment.Exit(0); + } + else + { + // generate and save the password to disk + int length = 10; + string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+"; + var random = new Random(); + string password = new string(Enumerable.Repeat(chars, length).Select(s => s[random.Next(s.Length)]).ToArray()); + + File.WriteAllText(PasswordRecoveryFile, password); + + // reset the password + using (var scope = app.Services.CreateScope()) + { + var userManager = scope.ServiceProvider.GetRequiredService(); + if (await userManager.FindByNameAsync(Environment.GetEnvironmentVariable("recoveraccount"), CancellationToken.None) != null) + { + ApplicationUser User = await userManager.FindByEmailAsync(Environment.GetEnvironmentVariable("recoveraccount"), CancellationToken.None); + + //set user password + PasswordHasher ph = new PasswordHasher(); + User.PasswordHash = ph.HashPassword(User, password); + + await userManager.SetPasswordHashAsync(User, User.PasswordHash, CancellationToken.None); + + Logging.Log(Logging.LogType.Information, "Server Startup", "Password reset complete, remove the recoveraccount environment variable and RecoverAccount.text file to allow server start.", null, true); + + Environment.Exit(0); + } + else + { + Logging.Log(Logging.LogType.Critical, "Server Startup", "Account to recover not found.", null, true); + + Environment.Exit(0); + } + } + + } +} +else +{ + // check if RecoverAccount.text file is present + if (File.Exists(PasswordRecoveryFile)) + { + // cannot start while password recovery file exists + Logging.Log(Logging.LogType.Critical, "Server Startup", "Unable to start while RecoverAccount.txt file exists. Remove the file and try again.", null, true); + Environment.Exit(0); + } +} + // setup library directories Config.LibraryConfiguration.InitLibrary(); diff --git a/gaseous-server/Reference/OldAccountController.cs.bak b/gaseous-server/Reference/OldAccountController.cs.bak new file mode 100644 index 0000000..3bad3c1 --- /dev/null +++ b/gaseous-server/Reference/OldAccountController.cs.bak @@ -0,0 +1,585 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Security.Claims; +using Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; + +namespace gaseous_server.Controllers +{ + [ApiController] + [Route("Account")] + [Authorize] + public class OldAccountController : Controller + { + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public OldAccountController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ILoggerFactory loggerFactory) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _logger = loggerFactory.CreateLogger(); + } + + // + // GET: /Account/Login + [HttpGet] + [AllowAnonymous] + [Route("Login")] + public IActionResult Login(string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + // + // POST: /Account/Login + [HttpPost] + [AllowAnonymous] + [Route("Login")] + public async Task Login(LoginViewModel model, string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + // This doesn't count login failures towards account lockout + // To enable password failures to trigger account lockout, set lockoutOnFailure: true + var result = await _signInManager.PasswordSignInAsync(model.Email, model.Password, model.RememberMe, lockoutOnFailure: false); + if (result.Succeeded) + { + _logger.LogInformation(1, "User logged in."); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl, RememberMe = model.RememberMe }); + } + if (result.IsLockedOut) + { + _logger.LogWarning(2, "User account locked out."); + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid login attempt."); + return View(model); + } + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/Register + [HttpGet] + [AllowAnonymous] + [Route("Register")] + public IActionResult Register(string returnUrl = null) + { + ViewData["ReturnUrl"] = returnUrl; + return View(); + } + + // + // POST: /Account/Register + [HttpPost] + [AllowAnonymous] + [Route("Register")] + public async Task Register(RegisterViewModel model, string returnUrl = null) + { + // ViewData["ReturnUrl"] = returnUrl; + if (ModelState.IsValid) + { + ApplicationUser user = new ApplicationUser + { + UserName = model.UserName, + NormalizedUserName = model.UserName.ToUpper(), + Email = model.Email, + NormalizedEmail = model.Email.ToUpper() + }; + var result = await _userManager.CreateAsync(user, model.Password); + if (result.Succeeded) + { + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); + //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", + // "Please confirm your account by clicking this link: link"); + + // add all users to the member role + await _userManager.AddToRoleAsync(user, "Player"); + + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(3, "User created a new account with password."); + return RedirectToLocal(returnUrl); + } + AddErrors(result); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // POST: /Account/LogOff + [HttpPost] + [Route("LogOff")] + public async Task LogOff() + { + await _signInManager.SignOutAsync(); + _logger.LogInformation(4, "User logged out."); + //return RedirectToAction(nameof(HomeController.Index), "Home"); + return Ok(); + } + + // + // POST: /Account/ExternalLogin + [HttpPost] + [AllowAnonymous] + [Route("ExternalLogin")] + public IActionResult ExternalLogin(string provider, string returnUrl = null) + { + // Request a redirect to the external login provider. + var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl }); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl); + return Challenge(properties, provider); + } + + // + // GET: /Account/ExternalLoginCallback + [HttpGet] + [AllowAnonymous] + [Route("ExternalLoginCallback")] + public async Task ExternalLoginCallback(string returnUrl = null, string remoteError = null) + { + if (remoteError != null) + { + ModelState.AddModelError(string.Empty, $"Error from external provider: {remoteError}"); + return View(nameof(Login)); + } + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return RedirectToAction(nameof(Login)); + } + + // Sign in the user with this external login provider if the user already has a login. + var result = await _signInManager.ExternalLoginSignInAsync(info.LoginProvider, info.ProviderKey, isPersistent: false); + if (result.Succeeded) + { + // Update any authentication tokens if login succeeded + await _signInManager.UpdateExternalAuthenticationTokensAsync(info); + + _logger.LogInformation(5, "User logged in with {Name} provider.", info.LoginProvider); + return RedirectToLocal(returnUrl); + } + if (result.RequiresTwoFactor) + { + return RedirectToAction(nameof(SendCode), new { ReturnUrl = returnUrl }); + } + if (result.IsLockedOut) + { + return View("Lockout"); + } + else + { + // If the user does not have an account, then ask the user to create an account. + ViewData["ReturnUrl"] = returnUrl; + ViewData["ProviderDisplayName"] = info.ProviderDisplayName; + var email = info.Principal.FindFirstValue(ClaimTypes.Email); + return View("ExternalLoginConfirmation", new ExternalLoginConfirmationViewModel { Email = email }); + } + } + + // + // POST: /Account/ExternalLoginConfirmation + [HttpPost] + [AllowAnonymous] + [Route("ExternalLoginConfirmation")] + public async Task ExternalLoginConfirmation(ExternalLoginConfirmationViewModel model, string returnUrl = null) + { + if (ModelState.IsValid) + { + // Get the information about the user from the external login provider + var info = await _signInManager.GetExternalLoginInfoAsync(); + if (info == null) + { + return View("ExternalLoginFailure"); + } + var user = new ApplicationUser { UserName = model.Email, Email = model.Email }; + var result = await _userManager.CreateAsync(user); + if (result.Succeeded) + { + result = await _userManager.AddLoginAsync(user, info); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(6, "User created an account using {Name} provider.", info.LoginProvider); + + // Update any authentication tokens as well + await _signInManager.UpdateExternalAuthenticationTokensAsync(info); + + return RedirectToLocal(returnUrl); + } + } + AddErrors(result); + } + + ViewData["ReturnUrl"] = returnUrl; + return View(model); + } + + // GET: /Account/ConfirmEmail + [HttpGet] + [AllowAnonymous] + [Route("ConfirmEmail")] + public async Task ConfirmEmail(string userId, string code) + { + if (userId == null || code == null) + { + return View("Error"); + } + var user = await _userManager.FindByIdAsync(userId); + if (user == null) + { + return View("Error"); + } + var result = await _userManager.ConfirmEmailAsync(user, code); + return View(result.Succeeded ? "ConfirmEmail" : "Error"); + } + + // + // GET: /Account/ForgotPassword + [HttpGet] + [AllowAnonymous] + [Route("ForgotPassword")] + public IActionResult ForgotPassword() + { + return View(); + } + + // + // POST: /Account/ForgotPassword + [HttpPost] + [AllowAnonymous] + [Route("ForgotPassword")] + public async Task ForgotPassword(ForgotPasswordViewModel model) + { + if (ModelState.IsValid) + { + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null || !(await _userManager.IsEmailConfirmedAsync(user))) + { + // Don't reveal that the user does not exist or is not confirmed + return View("ForgotPasswordConfirmation"); + } + + // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 + // Send an email with this link + //var code = await _userManager.GeneratePasswordResetTokenAsync(user); + //var callbackUrl = Url.Action("ResetPassword", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); + //await _emailSender.SendEmailAsync(model.Email, "Reset Password", + // "Please reset your password by clicking here: link"); + //return View("ForgotPasswordConfirmation"); + } + + // If we got this far, something failed, redisplay form + return View(model); + } + + // + // GET: /Account/ForgotPasswordConfirmation + [HttpGet] + [AllowAnonymous] + [Route("ForgotPasswordConfirmation")] + public IActionResult ForgotPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/ResetPassword + [HttpGet] + [AllowAnonymous] + [Route("ResetPassword")] + public IActionResult ResetPassword(string code = null) + { + return code == null ? View("Error") : View(); + } + + // + // POST: /Account/ResetPassword + [HttpPost] + [AllowAnonymous] + [Route("ResetPassword")] + public async Task ResetPassword(ResetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await _userManager.FindByEmailAsync(model.Email); + if (user == null) + { + // Don't reveal that the user does not exist + return RedirectToAction(nameof(OldAccountController.ResetPasswordConfirmation), "Account"); + } + var result = await _userManager.ResetPasswordAsync(user, model.Code, model.Password); + if (result.Succeeded) + { + return RedirectToAction(nameof(OldAccountController.ResetPasswordConfirmation), "Account"); + } + AddErrors(result); + return View(); + } + + // + // GET: /Account/ResetPasswordConfirmation + [HttpGet] + [AllowAnonymous] + [Route("ResetPasswordConfirmation")] + public IActionResult ResetPasswordConfirmation() + { + return View(); + } + + // + // GET: /Account/SendCode + [HttpGet] + [AllowAnonymous] + [Route("SendCode")] + public async Task SendCode(string returnUrl = null, bool rememberMe = false) + { + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + var userFactors = await _userManager.GetValidTwoFactorProvidersAsync(user); + var factorOptions = userFactors.Select(purpose => new SelectListItem { Text = purpose, Value = purpose }).ToList(); + return View(new SendCodeViewModel { Providers = factorOptions, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/SendCode + [HttpPost] + [AllowAnonymous] + [Route("SendCode")] + public async Task SendCode(SendCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(); + } + + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + + if (model.SelectedProvider == "Authenticator") + { + return RedirectToAction(nameof(VerifyAuthenticatorCode), new { ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // Generate the token and send it + var code = await _userManager.GenerateTwoFactorTokenAsync(user, model.SelectedProvider); + if (string.IsNullOrWhiteSpace(code)) + { + return View("Error"); + } + + var message = "Your security code is: " + code; + if (model.SelectedProvider == "Email") + { + await _emailSender.SendEmailAsync(await _userManager.GetEmailAsync(user), "Security Code", message); + } + // else if (model.SelectedProvider == "Phone") + // { + // await _smsSender.SendSmsAsync(await _userManager.GetPhoneNumberAsync(user), message); + // } + + return RedirectToAction(nameof(VerifyCode), new { Provider = model.SelectedProvider, ReturnUrl = model.ReturnUrl, RememberMe = model.RememberMe }); + } + + // + // GET: /Account/VerifyCode + [HttpGet] + [AllowAnonymous] + [Route("VerifyCode")] + public async Task VerifyCode(string provider, bool rememberMe, string returnUrl = null) + { + // Require that the user has already logged in via username/password or external login + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + return View(new VerifyCodeViewModel { Provider = provider, ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/VerifyCode + [HttpPost] + [AllowAnonymous] + [Route("VerifyCode")] + public async Task VerifyCode(VerifyCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + // The following code protects for brute force attacks against the two factor codes. + // If a user enters incorrect codes for a specified amount of time then the user account + // will be locked out for a specified amount of time. + var result = await _signInManager.TwoFactorSignInAsync(model.Provider, model.Code, model.RememberMe, model.RememberBrowser); + if (result.Succeeded) + { + return RedirectToLocal(model.ReturnUrl); + } + if (result.IsLockedOut) + { + _logger.LogWarning(7, "User account locked out."); + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid code."); + return View(model); + } + } + + // + // GET: /Account/VerifyAuthenticatorCode + [HttpGet] + [AllowAnonymous] + [Route("VerifyAuthenticatorCode")] + public async Task VerifyAuthenticatorCode(bool rememberMe, string returnUrl = null) + { + // Require that the user has already logged in via username/password or external login + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + return View(new VerifyAuthenticatorCodeViewModel { ReturnUrl = returnUrl, RememberMe = rememberMe }); + } + + // + // POST: /Account/VerifyAuthenticatorCode + [HttpPost] + [AllowAnonymous] + [Route("VerifyAuthenticatorCode")] + public async Task VerifyAuthenticatorCode(VerifyAuthenticatorCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + // The following code protects for brute force attacks against the two factor codes. + // If a user enters incorrect codes for a specified amount of time then the user account + // will be locked out for a specified amount of time. + var result = await _signInManager.TwoFactorAuthenticatorSignInAsync(model.Code, model.RememberMe, model.RememberBrowser); + if (result.Succeeded) + { + return RedirectToLocal(model.ReturnUrl); + } + if (result.IsLockedOut) + { + _logger.LogWarning(7, "User account locked out."); + return View("Lockout"); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid code."); + return View(model); + } + } + + // + // GET: /Account/UseRecoveryCode + [HttpGet] + [AllowAnonymous] + [Route("UseRecoveryCode")] + public async Task UseRecoveryCode(string returnUrl = null) + { + // Require that the user has already logged in via username/password or external login + var user = await _signInManager.GetTwoFactorAuthenticationUserAsync(); + if (user == null) + { + return View("Error"); + } + return View(new UseRecoveryCodeViewModel { ReturnUrl = returnUrl }); + } + + // + // POST: /Account/UseRecoveryCode + [HttpPost] + [AllowAnonymous] + [Route("UseRecoveryCode")] + public async Task UseRecoveryCode(UseRecoveryCodeViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var result = await _signInManager.TwoFactorRecoveryCodeSignInAsync(model.Code); + if (result.Succeeded) + { + return RedirectToLocal(model.ReturnUrl); + } + else + { + ModelState.AddModelError(string.Empty, "Invalid code."); + return View(model); + } + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + private Task GetCurrentUserAsync() + { + return _userManager.GetUserAsync(HttpContext.User); + } + + private IActionResult RedirectToLocal(string returnUrl) + { + if (Url.IsLocalUrl(returnUrl)) + { + return Redirect(returnUrl); + } + else + { + return Redirect("/"); + } + } + + #endregion + } +} \ No newline at end of file diff --git a/gaseous-server/Reference/OldManageController.cs.bak b/gaseous-server/Reference/OldManageController.cs.bak new file mode 100644 index 0000000..2a2afbd --- /dev/null +++ b/gaseous-server/Reference/OldManageController.cs.bak @@ -0,0 +1,383 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Authentication; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Identity; +using Microsoft.AspNetCore.Identity.UI.Services; +using Microsoft.AspNetCore.Mvc; + +namespace gaseous_server.Controllers; + +[ApiController] +[Route("Manage")] +[Authorize] +public class OldManageController : Controller +{ + private readonly UserManager _userManager; + private readonly SignInManager _signInManager; + private readonly IEmailSender _emailSender; + private readonly ILogger _logger; + + public OldManageController( + UserManager userManager, + SignInManager signInManager, + IEmailSender emailSender, + ILoggerFactory loggerFactory) + { + _userManager = userManager; + _signInManager = signInManager; + _emailSender = emailSender; + _logger = loggerFactory.CreateLogger(); + } + + // + // GET: /Manage/Index + [HttpGet] + [Route("Index")] + public async Task Index(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.ChangePasswordSuccess ? "Your password has been changed." + : message == ManageMessageId.SetPasswordSuccess ? "Your password has been set." + : message == ManageMessageId.SetTwoFactorSuccess ? "Your two-factor authentication provider has been set." + : message == ManageMessageId.Error ? "An error has occurred." + : message == ManageMessageId.AddPhoneSuccess ? "Your phone number was added." + : message == ManageMessageId.RemovePhoneSuccess ? "Your phone number was removed." + : ""; + + var user = await GetCurrentUserAsync(); + var model = new IndexViewModel + { + HasPassword = await _userManager.HasPasswordAsync(user), + PhoneNumber = await _userManager.GetPhoneNumberAsync(user), + TwoFactor = await _userManager.GetTwoFactorEnabledAsync(user), + Logins = await _userManager.GetLoginsAsync(user), + BrowserRemembered = await _signInManager.IsTwoFactorClientRememberedAsync(user), + AuthenticatorKey = await _userManager.GetAuthenticatorKeyAsync(user) + }; + return View(model); + } + + // + // POST: /Manage/RemoveLogin + [HttpPost] + [Route("RemoveLogin")] + public async Task RemoveLogin(RemoveLoginViewModel account) + { + ManageMessageId? message = ManageMessageId.Error; + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.RemoveLoginAsync(user, account.LoginProvider, account.ProviderKey); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + message = ManageMessageId.RemoveLoginSuccess; + } + } + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + // + // GET: /Manage/AddPhoneNumber + [HttpGet] + [Route("AddPhoneNumber")] + public IActionResult AddPhoneNumber() + { + return View(); + } + + // + // POST: /Manage/AddPhoneNumber + [HttpPost] + [Route("AddPhoneNumber")] + public async Task AddPhoneNumber(AddPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + // Generate the token and send it + var user = await GetCurrentUserAsync(); + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(user, model.PhoneNumber); + //await _smsSender.SendSmsAsync(model.PhoneNumber, "Your security code is: " + code); + return RedirectToAction(nameof(VerifyPhoneNumber), new { PhoneNumber = model.PhoneNumber }); + } + + // + // POST: /Manage/ResetAuthenticatorKey + [HttpPost] + [Route("ResetAuthenticatorKey")] + public async Task ResetAuthenticatorKey() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.ResetAuthenticatorKeyAsync(user); + _logger.LogInformation(1, "User reset authenticator key."); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // POST: /Manage/GenerateRecoveryCode + [HttpPost] + [Route("GenerateRecoveryCode")] + public async Task GenerateRecoveryCode() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + var codes = await _userManager.GenerateNewTwoFactorRecoveryCodesAsync(user, 5); + _logger.LogInformation(1, "User generated new recovery code."); + return View("DisplayRecoveryCodes", new DisplayRecoveryCodesViewModel { Codes = codes }); + } + return View("Error"); + } + + // + // POST: /Manage/EnableTwoFactorAuthentication + [HttpPost] + [Route("EnableTwoFactorAuthentication")] + public async Task EnableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, true); + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(1, "User enabled two-factor authentication."); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // POST: /Manage/DisableTwoFactorAuthentication + [HttpPost] + [Route("DisableTwoFactorAuthentication")] + public async Task DisableTwoFactorAuthentication() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + await _userManager.SetTwoFactorEnabledAsync(user, false); + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(2, "User disabled two-factor authentication."); + } + return RedirectToAction(nameof(Index), "Manage"); + } + + // + // GET: /Manage/VerifyPhoneNumber + [HttpGet] + [Route("VerifyPhoneNumber")] + public async Task VerifyPhoneNumber(string phoneNumber) + { + var code = await _userManager.GenerateChangePhoneNumberTokenAsync(await GetCurrentUserAsync(), phoneNumber); + // Send an SMS to verify the phone number + return phoneNumber == null ? View("Error") : View(new VerifyPhoneNumberViewModel { PhoneNumber = phoneNumber }); + } + + // + // POST: /Manage/VerifyPhoneNumber + [HttpPost] + [Route("VerifyPhoneNumber")] + public async Task VerifyPhoneNumber(VerifyPhoneNumberViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePhoneNumberAsync(user, model.PhoneNumber, model.Code); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.AddPhoneSuccess }); + } + } + // If we got this far, something failed, redisplay the form + ModelState.AddModelError(string.Empty, "Failed to verify phone number"); + return View(model); + } + + // + // GET: /Manage/RemovePhoneNumber + [HttpPost] + [Route("RemovePhoneNumber")] + public async Task RemovePhoneNumber() + { + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.SetPhoneNumberAsync(user, null); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.RemovePhoneSuccess }); + } + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/ChangePassword + [HttpGet] + [Route("ChangePassword")] + public IActionResult ChangePassword() + { + return View(); + } + + // + // POST: /Manage/ChangePassword + [HttpPost] + [Route("ChangePassword")] + public async Task ChangePassword(ChangePasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.ChangePasswordAsync(user, model.OldPassword, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + _logger.LogInformation(3, "User changed their password successfully."); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.ChangePasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + // + // GET: /Manage/SetPassword + [HttpGet] + [Route("SetPassword")] + public IActionResult SetPassword() + { + return View(); + } + + // + // POST: /Manage/SetPassword + [HttpPost] + [Route("SetPassword")] + public async Task SetPassword(SetPasswordViewModel model) + { + if (!ModelState.IsValid) + { + return View(model); + } + + var user = await GetCurrentUserAsync(); + if (user != null) + { + var result = await _userManager.AddPasswordAsync(user, model.NewPassword); + if (result.Succeeded) + { + await _signInManager.SignInAsync(user, isPersistent: false); + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.SetPasswordSuccess }); + } + AddErrors(result); + return View(model); + } + return RedirectToAction(nameof(Index), new { Message = ManageMessageId.Error }); + } + + //GET: /Manage/ManageLogins + [HttpGet] + [Route("ManageLogins")] + public async Task ManageLogins(ManageMessageId? message = null) + { + ViewData["StatusMessage"] = + message == ManageMessageId.RemoveLoginSuccess ? "The external login was removed." + : message == ManageMessageId.AddLoginSuccess ? "The external login was added." + : message == ManageMessageId.Error ? "An error has occurred." + : ""; + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var userLogins = await _userManager.GetLoginsAsync(user); + var schemes = await _signInManager.GetExternalAuthenticationSchemesAsync(); + var otherLogins = schemes.Where(auth => userLogins.All(ul => auth.Name != ul.LoginProvider)).ToList(); + ViewData["ShowRemoveButton"] = user.PasswordHash != null || userLogins.Count > 1; + return View(new ManageLoginsViewModel + { + CurrentLogins = userLogins, + OtherLogins = otherLogins + }); + } + + // + // POST: /Manage/LinkLogin + [HttpPost] + [Route("LinkLogin")] + public IActionResult LinkLogin(string provider) + { + // Request a redirect to the external login provider to link a login for the current user + var redirectUrl = Url.Action("LinkLoginCallback", "Manage"); + var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl, _userManager.GetUserId(User)); + return Challenge(properties, provider); + } + + // + // GET: /Manage/LinkLoginCallback + [HttpGet] + [Route("LinkLoginCallback")] + public async Task LinkLoginCallback() + { + var user = await GetCurrentUserAsync(); + if (user == null) + { + return View("Error"); + } + var info = await _signInManager.GetExternalLoginInfoAsync(await _userManager.GetUserIdAsync(user)); + if (info == null) + { + return RedirectToAction(nameof(ManageLogins), new { Message = ManageMessageId.Error }); + } + var result = await _userManager.AddLoginAsync(user, info); + var message = result.Succeeded ? ManageMessageId.AddLoginSuccess : ManageMessageId.Error; + return RedirectToAction(nameof(ManageLogins), new { Message = message }); + } + + #region Helpers + + private void AddErrors(IdentityResult result) + { + foreach (var error in result.Errors) + { + ModelState.AddModelError(string.Empty, error.Description); + } + } + + public enum ManageMessageId + { + AddPhoneSuccess, + AddLoginSuccess, + ChangePasswordSuccess, + SetTwoFactorSuccess, + SetPasswordSuccess, + RemoveLoginSuccess, + RemovePhoneSuccess, + Error + } + + private Task GetCurrentUserAsync() + { + return _userManager.GetUserAsync(HttpContext.User); + } + + #endregion +} \ No newline at end of file diff --git a/gaseous-server/Support/Database/MySQL/gaseous-1005.sql b/gaseous-server/Support/Database/MySQL/gaseous-1005.sql new file mode 100644 index 0000000..ddcd3b2 --- /dev/null +++ b/gaseous-server/Support/Database/MySQL/gaseous-1005.sql @@ -0,0 +1,54 @@ +CREATE TABLE `roles` ( + `Id` varchar(128) NOT NULL, + `Name` varchar(256) NOT NULL, + PRIMARY KEY (`Id`) +); + +CREATE TABLE `users` ( + `Id` varchar(128) NOT NULL, + `Email` varchar(256) DEFAULT NULL, + `EmailConfirmed` tinyint(1) NOT NULL, + `NormalizedEmail` varchar(256) DEFAULT NULL, + `PasswordHash` longtext, + `SecurityStamp` longtext, + `ConcurrencyStamp` longtext, + `PhoneNumber` longtext, + `PhoneNumberConfirmed` tinyint(1) NOT NULL, + `TwoFactorEnabled` tinyint(1) NOT NULL, + `LockoutEnd` datetime DEFAULT NULL, + `LockoutEnabled` tinyint(1) NOT NULL, + `AccessFailedCount` int(11) NOT NULL, + `UserName` varchar(256) NOT NULL, + `NormalizedUserName` varchar(256) NOT NULL, + `SecurityProfile` longtext, + PRIMARY KEY (`Id`) +); + +CREATE TABLE `userclaims` ( + `Id` int(11) NOT NULL AUTO_INCREMENT, + `UserId` varchar(128) NOT NULL, + `ClaimType` longtext, + `ClaimValue` longtext, + PRIMARY KEY (`Id`), + UNIQUE KEY `Id` (`Id`), + KEY `UserId` (`UserId`), + CONSTRAINT `ApplicationUser_Claims` FOREIGN KEY (`UserId`) REFERENCES `users` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE `userlogins` ( + `LoginProvider` varchar(128) NOT NULL, + `ProviderKey` varchar(128) NOT NULL, + `UserId` varchar(128) NOT NULL, + PRIMARY KEY (`LoginProvider`,`ProviderKey`,`UserId`), + KEY `ApplicationUser_Logins` (`UserId`), + CONSTRAINT `ApplicationUser_Logins` FOREIGN KEY (`UserId`) REFERENCES `users` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION +); + +CREATE TABLE `userroles` ( + `UserId` varchar(128) NOT NULL, + `RoleId` varchar(128) NOT NULL, + PRIMARY KEY (`UserId`,`RoleId`), + KEY `IdentityRole_Users` (`RoleId`), + CONSTRAINT `ApplicationUser_Roles` FOREIGN KEY (`UserId`) REFERENCES `users` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION, + CONSTRAINT `IdentityRole_Users` FOREIGN KEY (`RoleId`) REFERENCES `roles` (`Id`) ON DELETE CASCADE ON UPDATE NO ACTION +) ; diff --git a/gaseous-server/gaseous-server.csproj b/gaseous-server/gaseous-server.csproj index 4e197ed..aeadbcd 100644 --- a/gaseous-server/gaseous-server.csproj +++ b/gaseous-server/gaseous-server.csproj @@ -20,9 +20,11 @@ + + @@ -43,6 +45,7 @@ + @@ -114,6 +117,7 @@ + @@ -169,5 +173,6 @@ + diff --git a/gaseous-server/wwwroot/.DS_Store b/gaseous-server/wwwroot/.DS_Store index 43dca08..44dbb97 100644 Binary files a/gaseous-server/wwwroot/.DS_Store and b/gaseous-server/wwwroot/.DS_Store differ diff --git a/gaseous-server/wwwroot/images/LoginWallpaper.jpg b/gaseous-server/wwwroot/images/LoginWallpaper.jpg new file mode 100644 index 0000000..3a9fc3b Binary files /dev/null and b/gaseous-server/wwwroot/images/LoginWallpaper.jpg differ diff --git a/gaseous-server/wwwroot/images/tick.svg b/gaseous-server/wwwroot/images/tick.svg new file mode 100644 index 0000000..930e337 --- /dev/null +++ b/gaseous-server/wwwroot/images/tick.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/images/user.svg b/gaseous-server/wwwroot/images/user.svg new file mode 100644 index 0000000..13efef2 --- /dev/null +++ b/gaseous-server/wwwroot/images/user.svg @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/index.html b/gaseous-server/wwwroot/index.html index 42f2b99..a9e0189 100644 --- a/gaseous-server/wwwroot/index.html +++ b/gaseous-server/wwwroot/index.html @@ -2,7 +2,7 @@ - + @@ -38,6 +38,8 @@ head.appendChild(newLink); } } + + var userProfile; @@ -45,24 +47,35 @@ \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html b/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html new file mode 100644 index 0000000..db936ab --- /dev/null +++ b/gaseous-server/wwwroot/pages/dialogs/settingsuseredit.html @@ -0,0 +1,375 @@ +
+
Password
+
Role
+
Content Restrictions
+ +
+ +
+ + + + + +
+
+ +
+ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html b/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html new file mode 100644 index 0000000..ccacd4a --- /dev/null +++ b/gaseous-server/wwwroot/pages/dialogs/settingsusernew.html @@ -0,0 +1,88 @@ +

New User

+ + + + + + + + + + + + + + + + + + + + +
+ Email + + +
+ Password + + +
+ Confirm password + + +
+ +
+ + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/upload.html b/gaseous-server/wwwroot/pages/dialogs/upload.html index 00d97f3..3eef481 100644 --- a/gaseous-server/wwwroot/pages/dialogs/upload.html +++ b/gaseous-server/wwwroot/pages/dialogs/upload.html @@ -28,7 +28,7 @@ document.getElementById('upload_platformoverride').innerHTML = ""; var myDropzone = new Dropzone("div#upload_target", { - url: "/api/v1.0/Roms", + url: "/api/v1.1/Roms", autoProcessQueue: true, uploadMultiple: true, paramName: myParamName, @@ -84,7 +84,7 @@ $('#upload_platformoverride').select2({ minimumInputLength: 3, ajax: { - url: '/api/v1.0/Search/Platform', + url: '/api/v1.1/Search/Platform', data: function (params) { var query = { SearchString: params.term @@ -125,6 +125,6 @@ } console.log(queryString); - myDropzone.options.url = "/api/v1.0/Roms" + queryString; + myDropzone.options.url = "/api/v1.1/Roms" + queryString; }); \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/dialogs/userprofile.html b/gaseous-server/wwwroot/pages/dialogs/userprofile.html new file mode 100644 index 0000000..56655a9 --- /dev/null +++ b/gaseous-server/wwwroot/pages/dialogs/userprofile.html @@ -0,0 +1,122 @@ +
+
Account
+
+
+ +
+ + diff --git a/gaseous-server/wwwroot/pages/emulator.html b/gaseous-server/wwwroot/pages/emulator.html index cd37b98..266bef8 100644 --- a/gaseous-server/wwwroot/pages/emulator.html +++ b/gaseous-server/wwwroot/pages/emulator.html @@ -15,7 +15,7 @@ var emuBios = ''; var emuBackground = ''; - ajaxCall('/api/v1.0/Games/' + gameId, 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId, 'GET', function (result) { gameData = result; // load artwork @@ -27,22 +27,22 @@ } else { if (result.cover) { var bg = document.getElementById('bgImage'); - bg.setAttribute('style', 'background-image: url("/api/v1.0/Games/' + gameId + '/cover/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); + bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/cover/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); } } if (result.cover) { - emuBackground = '/api/v1.0/Games/' + gameId + '/cover/image'; + emuBackground = '/api/v1.1/Games/' + gameId + '/cover/image'; } emuGameTitle = gameData.name; }); - ajaxCall('/api/v1.0/Bios/' + platformId, 'GET', function (result) { + ajaxCall('/api/v1.1/Bios/' + platformId, 'GET', function (result) { if (result.length == 0) { emuBios = ''; } else { - emuBios = '/api/v1.0/Bios/zip/' + platformId; + emuBios = '/api/v1.1/Bios/zip/' + platformId; } switch (getQueryString('engine', 'string')) { @@ -59,7 +59,7 @@ artworksPosition = 0; } var bg = document.getElementById('bgImage'); - bg.setAttribute('style', 'background-image: url("/api/v1.0/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); + bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); } } diff --git a/gaseous-server/wwwroot/pages/first.html b/gaseous-server/wwwroot/pages/first.html new file mode 100644 index 0000000..17813d1 --- /dev/null +++ b/gaseous-server/wwwroot/pages/first.html @@ -0,0 +1,167 @@ + + + + + + + + + + + + + + + + + + + + + + + Gaseous Games + + + + +
+
+
+ +
+
+
+ + +
Gaseous Games
+ + +
+
+ +
+ +
+ Wallpaper by Joey Kwok / Unsplash +
+ + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/game.html b/gaseous-server/wwwroot/pages/game.html index d8e26b5..04ed58f 100644 --- a/gaseous-server/wwwroot/pages/game.html +++ b/gaseous-server/wwwroot/pages/game.html @@ -94,7 +94,7 @@ var artworksTimer = null; var selectedScreenshot = 0; - ajaxCall('/api/v1.0/Games/' + gameId, 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId, 'GET', function (result) { // populate games page gameData = result; @@ -116,7 +116,7 @@ // get alt name var gameTitleAltLabel = document.getElementById('gametitle_alts'); if (result.alternativeNames) { - ajaxCall('/api/v1.0/Games/' + gameId + '/alternativename', 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/alternativename', 'GET', function (result) { var altNames = ''; for (var i = 0; i < result.length; i++) { if (altNames.length > 0) { @@ -161,7 +161,7 @@ } else { var bg = document.getElementById('bgImage'); if (result.cover) { - bg.setAttribute('style', 'background-image: url("/api/v1.0/Games/' + gameId + '/cover/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); + bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/cover/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); } else { var randomInt = randomIntFromInterval(1, 3); bg.setAttribute('style', 'background-image: url("/images/gamebg' + randomInt + '.jpg"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); @@ -175,7 +175,7 @@ var gameDeveloperLoaded = false; var gamePublisherLoaded = false; if (result.involvedCompanies) { - ajaxCall('/api/v1.0/games/' + gameId + '/companies', 'GET', function (result) { + ajaxCall('/api/v1.1/games/' + gameId + '/companies', 'GET', function (result) { var lstDevelopers = []; var lstPublishers = []; @@ -227,7 +227,7 @@ var gameImage = document.createElement('img'); gameImage.className = 'game_cover_image'; if (result.cover) { - gameImage.src = '/api/v1.0/Games/' + result.id + '/cover/image'; + gameImage.src = '/api/v1.1/Games/' + result.id + '/cover/image'; } else { gameImage.src = '/images/unknowngame.png'; gameImage.className = 'game_cover_image unknown'; @@ -240,7 +240,7 @@ var gameRatings = document.createElement('div'); for (var i = 0; i < result.ageRatings.ids.length; i++) { var ratingImage = document.createElement('img'); - ratingImage.src = '/api/v1.0/Games/' + result.id + '/agerating/' + result.ageRatings.ids[i] + '/image'; + ratingImage.src = '/api/v1.1/Games/' + result.id + '/agerating/' + result.ageRatings.ids[i] + '/image'; ratingImage.className = 'rating_image'; gameRatings.appendChild(ratingImage); } @@ -252,7 +252,7 @@ // load genres var gameSummaryGenres = document.getElementById('gamesumarry_genres'); if (result.genres) { - ajaxCall('/api/v1.0/Games/' + gameId + '/genre', 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/genre', 'GET', function (result) { for (var i = 0; i < result.length; i++) { var genreLabel = document.createElement('span'); genreLabel.className = 'gamegenrelabel'; @@ -281,7 +281,7 @@ var screenshotItem = document.createElement('div'); screenshotItem.id = 'gamescreenshots_gallery_' + imageIndex; screenshotItem.setAttribute('name', 'gamescreenshots_gallery_item'); - screenshotItem.setAttribute('style', 'background-image: url("/api/v1.0/Games/' + gameId + '/screenshots/' + result.screenshots.ids[i] + '/image"); background-position: center; background-repeat: no-repeat; background-size: contain;)'); + screenshotItem.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/screenshots/' + result.screenshots.ids[i] + '/image"); background-position: center; background-repeat: no-repeat; background-size: contain;)'); screenshotItem.setAttribute('imageid', imageIndex); screenshotItem.setAttribute('imagetype', 0); screenshotItem.className = 'gamescreenshots_gallery_item'; @@ -293,7 +293,7 @@ // load videos if (result.videos) { - ajaxCall('/api/v1.0/Games/' + gameId + '/videos', 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/videos', 'GET', function (result) { var gameScreenshots_vGallery = document.getElementById('gamescreenshots_gallery_panel'); for (var i = 0; i < result.length; i++) { var vScreenshotItem = document.createElement('div'); @@ -360,7 +360,7 @@ } var gameRoms = document.getElementById('gamesummaryroms'); - ajaxCall('/api/v1.0/Games/' + gameId + '/roms', 'GET', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/roms', 'GET', function (result) { if (result.gameRomItems) { var gameRomItems = result.gameRomItems; var mediaGroups = result.mediaGroups; @@ -391,14 +391,14 @@ if (result.gameRomItems[i].emulator) { if (gameRomItems[i].emulator.type) { if (gameRomItems[i].emulator.type.length > 0) { - launchButton = 'Launch'; + launchButton = 'Launch'; } } } var newRow = [ ['', 'rom_checkbox_box_hidden', 'rom_edit_checkbox'], - '' + gameRomItems[i].name + '', + '' + gameRomItems[i].name + '', formatBytes(gameRomItems[i].size, 2), gameRomItems[i].romTypeMedia, gameRomItems[i].mediaLabel, @@ -436,7 +436,7 @@ if (gameRomItem.platformId == mediaGroup.platformId) { if (gameRomItem.emulator) { if (gameRomItem.emulator.type.length > 0) { - launchButton = 'Launch'; + launchButton = 'Launch'; break; } } @@ -459,7 +459,7 @@ break; case "Completed": statusText = 'Available'; - downloadLink = ''; + downloadLink = ''; packageSize = formatBytes(mediaGroup.size); launchButtonContent = launchButton; break; @@ -525,7 +525,7 @@ artworksPosition = 0; } var bg = document.getElementById('bgImage'); - bg.setAttribute('style', 'background-image: url("/api/v1.0/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); + bg.setAttribute('style', 'background-image: url("/api/v1.1/Games/' + gameId + '/artwork/' + artworks[artworksPosition] + '/image"); background-position: center; background-repeat: no-repeat; background-size: cover; filter: blur(10px); -webkit-filter: blur(10px);'); artworksTimer = setTimeout(rotateBackground, 60000); } } @@ -677,7 +677,7 @@ minimumInputLength: 3, placeholder: "Platform", ajax: { - url: '/api/v1.0/Search/Platform', + url: '/api/v1.1/Search/Platform', data: function (params) { var query = { SearchString: params.term @@ -709,7 +709,7 @@ templateResult: DropDownRenderGameOption, placeholder: "Game", ajax: { - url: '/api/v1.0/Search/Game', + url: '/api/v1.1/Search/Game', data: function (params) { fixplatform = $('#rom_edit_fixplatform').select2('data'); @@ -762,7 +762,7 @@ if (rom_checks[i].checked == true) { var romId = rom_checks[i].getAttribute('data-romid'); remapCallCounter += 1; - ajaxCall('/api/v1.0/Games/' + gameId + '/roms/' + romId + '?NewPlatformId=' + fixplatform[0].id + '&NewGameId=' + fixgame[0].id, 'PATCH', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/roms/' + romId + '?NewPlatformId=' + fixplatform[0].id + '&NewGameId=' + fixgame[0].id, 'PATCH', function (result) { remapTitlesCallback(); }, function (result) { remapTitlesCallback(); @@ -804,7 +804,7 @@ if (rom_checks[i].checked == true) { var romId = rom_checks[i].getAttribute('data-romid'); remapCallCounter += 1; - ajaxCall('/api/v1.0/Games/' + gameId + '/roms/' + romId, 'DELETE', function (result) { + ajaxCall('/api/v1.1/Games/' + gameId + '/roms/' + romId, 'DELETE', function (result) { remapTitlesCallback(); }); } @@ -846,7 +846,7 @@ } ajaxCall( - '/api/v1.0/Games/' + gameId + '/romgroup?PlatformId=' + platformId, + '/api/v1.1/Games/' + gameId + '/romgroup?PlatformId=' + platformId, 'POST', function (result) { DisplayROMCheckboxes(false); diff --git a/gaseous-server/wwwroot/pages/home.html b/gaseous-server/wwwroot/pages/home.html index 0a241cf..3059352 100644 --- a/gaseous-server/wwwroot/pages/home.html +++ b/gaseous-server/wwwroot/pages/home.html @@ -8,10 +8,10 @@ \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/login.html b/gaseous-server/wwwroot/pages/login.html new file mode 100644 index 0000000..99d35de --- /dev/null +++ b/gaseous-server/wwwroot/pages/login.html @@ -0,0 +1,128 @@ + + + + + + + + + + + + + + + + + + + + + + + Gaseous Games + + + + +
+
+
+ +
+
+
+ + +
Gaseous Games
+ + + + + + + + + + + + + + + + +
Email
Password
+ +
+ +
+
+
+
+ +
+ Wallpaper by Joey Kwok / Unsplash +
+ + + \ No newline at end of file diff --git a/gaseous-server/wwwroot/pages/settings.html b/gaseous-server/wwwroot/pages/settings.html index eef90ff..0eb93a5 100644 --- a/gaseous-server/wwwroot/pages/settings.html +++ b/gaseous-server/wwwroot/pages/settings.html @@ -6,10 +6,11 @@
Settings
System
-
Settings
-
Platform Mapping
+ + +
Firmware
-
Logs
+
About
@@ -23,6 +24,16 @@
\ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/filterformating.js b/gaseous-server/wwwroot/scripts/filterformating.js index 0e5336a..b4e497d 100644 --- a/gaseous-server/wwwroot/scripts/filterformating.js +++ b/gaseous-server/wwwroot/scripts/filterformating.js @@ -171,7 +171,7 @@ function buildFilterPanelItem(filterType, itemString, friendlyItemString, tags) filterPanelItemCheckBoxItem.className = 'filter_panel_item_checkbox'; filterPanelItemCheckBoxItem.name = 'filter_' + filterType; filterPanelItemCheckBoxItem.setAttribute('filter_id', itemString); - filterPanelItemCheckBoxItem.setAttribute('oninput' , 'executeFilter();'); + filterPanelItemCheckBoxItem.setAttribute('oninput' , 'executeFilter1_1();'); if (checkState == true) { filterPanelItemCheckBoxItem.checked = true; } @@ -198,7 +198,7 @@ function executeFilterDelayed() { filterExecutor = null; } - filterExecutor = setTimeout(executeFilter, 1000); + filterExecutor = setTimeout(executeFilter1_1, 1000); } function executeFilter() { @@ -299,4 +299,82 @@ function buildFilterTag(tags) { } return boundingDiv; +} + +function executeFilter1_1() { + console.log("Execute filter 1.1"); + var minUserRating = -1; + var minUserRatingInput = document.getElementById('filter_panel_userrating_min'); + if (minUserRatingInput.value) { + minUserRating = minUserRatingInput.value; + } + setCookie(minUserRatingInput.id, minUserRatingInput.value); + + var maxUserRating = -1; + var maxUserRatingInput = document.getElementById('filter_panel_userrating_max'); + if (maxUserRatingInput.value) { + maxUserRating = maxUserRatingInput.value; + } + setCookie(maxUserRatingInput.id, maxUserRatingInput.value); + + // build filter model + var model = { + "Name": document.getElementById('filter_panel_search').value, + "Platform": GetFilterQuery1_1('platform'), + "Genre": GetFilterQuery1_1('genre'), + "GameMode": GetFilterQuery1_1('gamemmode'), + "PlayerPerspective": GetFilterQuery1_1('playerperspective'), + "Theme": GetFilterQuery1_1('theme'), + "GameRating": { + "MinimumRating": minUserRating, + "MinimumRatingCount": -1, + "MaximumRating": maxUserRating, + "MaximumRatingCount": -1, + "IncludeUnrated": true + }, + "GameAgeRating": { + "AgeGroupings": [ + "Child", + "Teen", + "Mature", + "Adult" + ], + "IncludeUnrated": true + }, + "Sorting": { + "SortBy": "NameThe", + "SortAscenting": true + } + }; + + console.log('Search model = ' + JSON.stringify(model)); + + ajaxCall( + '/api/v1.1/Games', + 'POST', + function (result) { + var gameElement = document.getElementById('games_library'); + formatGamesPanel(gameElement, result); + }, + function (error) { + console.log('An error occurred: ' + JSON.stringify(error)); + }, + JSON.stringify(model) + ); +} + +function GetFilterQuery1_1(filterName) { + var Filters = document.getElementsByName('filter_' + filterName); + var selections = []; + + for (var i = 0; i < Filters.length; i++) { + if (Filters[i].checked) { + setCookie(Filters[i].id, true); + selections.push(Filters[i].getAttribute('filter_id')); + } else { + setCookie(Filters[i].id, false); + } + } + + return selections; } \ No newline at end of file diff --git a/gaseous-server/wwwroot/scripts/gamesformating.js b/gaseous-server/wwwroot/scripts/gamesformating.js index 49a4a0c..09b2929 100644 --- a/gaseous-server/wwwroot/scripts/gamesformating.js +++ b/gaseous-server/wwwroot/scripts/gamesformating.js @@ -20,7 +20,7 @@ function renderGameIcon(gameObject, showTitle, showRatings) { var gameImage = document.createElement('img'); gameImage.className = 'game_tile_image lazy'; if (gameObject.cover) { - gameImage.setAttribute('data-src', '/api/v1.0/Games/' + gameObject.id + '/cover/image'); + gameImage.setAttribute('data-src', '/api/v1.1/Games/' + gameObject.id + '/cover/image'); } else { gameImage.src = '/images/unknowngame.png'; gameImage.className = 'game_tile_image unknown'; @@ -40,7 +40,7 @@ function renderGameIcon(gameObject, showTitle, showRatings) { ratingsSection.id = 'ratings_section'; for (var i = 0; i < gameObject.ageRatings.ids.length; i++) { var ratingImage = document.createElement('img'); - ratingImage.src = '/api/v1.0/Games/' + gameObject.id + '/agerating/' + gameObject.ageRatings.ids[i] + '/image'; + ratingImage.src = '/api/v1.1/Games/' + gameObject.id + '/agerating/' + gameObject.ageRatings.ids[i] + '/image'; ratingImage.className = 'rating_image_mini'; ratingsSection.appendChild(ratingImage); } diff --git a/gaseous-server/wwwroot/scripts/main.js b/gaseous-server/wwwroot/scripts/main.js index 9d84989..86ab2a0 100644 --- a/gaseous-server/wwwroot/scripts/main.js +++ b/gaseous-server/wwwroot/scripts/main.js @@ -373,4 +373,19 @@ function LoadEditableTableData(TableName, Headers, Values) { eTable.appendChild(row); } +} + +function CreateBadge(BadgeText, ColourOverride) { + var badgeItem = document.createElement('div'); + badgeItem.className = 'dropdownroleitem'; + badgeItem.innerHTML = BadgeText.toUpperCase(); + var colorVal = intToRGB(hashCode(BadgeText)); + if (!ColourOverride) { + badgeItem.style.backgroundColor = '#' + colorVal; + badgeItem.style.borderColor = '#' + colorVal; + } else { + badgeItem.style.backgroundColor = ColourOverride; + badgeItem.style.borderColor = ColourOverride; + } + return badgeItem; } \ No newline at end of file diff --git a/gaseous-server/wwwroot/styles/style.css b/gaseous-server/wwwroot/styles/style.css index 082b900..ca8dbba 100644 --- a/gaseous-server/wwwroot/styles/style.css +++ b/gaseous-server/wwwroot/styles/style.css @@ -30,12 +30,12 @@ h3 { .modal { display: none; /* Hidden by default */ position: fixed; /* Stay in place */ - z-index: 1; /* Sit on top */ + z-index: 100; /* Sit on top */ left: 0; top: 0; width: 100%; /* Full width */ height: 100%; /* Full height */ - overflow: auto; /* Enable scroll if needed */ + overflow: none; /* Enable scroll if needed */ background-color: rgb(0,0,0); /* Fallback color */ background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ backdrop-filter: blur(8px); @@ -47,9 +47,10 @@ h3 { /* Modal Content/Box */ .modal-content { background-color: #383838; - margin: 15% auto; /* 15% from the top and centered */ + margin: 10% auto; /* 15% from the top and centered */ padding: 10px; border: 1px solid #888; + border-radius: 10px; width: 700px; /* Could be more or less, depending on screen size */ min-height: 358px; } @@ -58,6 +59,7 @@ h3 { margin: 20% auto; /* 20% from the top and centered */ padding: 10px; border: 1px solid #888; + border-radius: 10px; width: 300px; /* Could be more or less, depending on screen size */ min-height: 110px; } @@ -205,7 +207,7 @@ h3 { z-index: 1; } -input[type='text'], input[type='number'] { +input[type='text'], input[type='number'], input[type="email"], input[type="password"] { background-color: #2b2b2b; color: white; padding: 5px; @@ -409,7 +411,7 @@ input[id='filter_panel_userrating_max'] { display: inline-block; max-width: 32px; max-height: 32px; - margin-right: 2px; + margin-right: 10px; } #gamescreenshots { @@ -621,14 +623,14 @@ th { height: 100%; } -div[name="properties_toc_item"] { +div[name="properties_toc_item"],div[name="properties_user_toc_item"],div[name="properties_profile_toc_item"] { padding: 10px; border-bottom-width: 1px; border-bottom-style: solid; border-bottom-color: #2b2b2b; } -div[name="properties_toc_item"]:hover { +div[name="properties_toc_item"]:hover,div[name="properties_user_toc_item"]:hover,div[name="properties_profile_toc_item"]:hover { background-color: #2b2b2b; cursor: pointer; } @@ -965,4 +967,98 @@ button:disabled { .romGroupTitles { padding-left: 20px; +} + +.loginwindow { + position: fixed; /* Stay in place */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + overflow: auto; /* Enable scroll if needed */ + /*background-color: rgb(0,0,0); /* Fallback color */ + /*background-color: rgba(0,0,0,0.4); /* Black w/ opacity */ + /*backdrop-filter: blur(8px); + -webkit-backdrop-filter: blur(8px);*/ + filter: drop-shadow(5px 5px 10px #000); + -webkit-filter: drop-shadow(5px 5px 10px #000); +} + +/* Modal Content/Box */ +.loginwindow-content { + position: relative; + background-color: #383838; + margin: 15% auto; /* 15% from the top and centered */ + padding: 10px; + border: 1px solid #888; + border-radius: 10px; + width: 350px; /* Could be more or less, depending on screen size */ + min-height: 250px; +} + +#loginwindow_header_label { + font-family: Commodore64; + display: inline-block; + padding: 10px; + font-size: 24pt; + vertical-align: top; + /*color: #edeffa;*/ + color: #7c70da; +} + +/* The container
- needed to position the dropdown content */ +.dropdown { + position: relative; + display: inline-block; + float: right; +} + +/* Dropdown Content (Hidden by Default) */ +.dropdown-content { + display: none; + position: absolute; + background-color: #f1f1f1; + min-width: 160px; + /* box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); */ + z-index: 1; + right: 0; + top: 40px; + filter: drop-shadow(5px 5px 10px #000); + -webkit-filter: drop-shadow(5px 5px 10px #000); +} + +/* Links inside the dropdown */ +.dropdown-content a, .dropdown-content span { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown-content span { + cursor: auto; +} + +/* Change color of dropdown links on hover */ +.dropdown-content a:hover {background-color: #ddd;} + +/* Show the dropdown menu (use JS to add this class to the .dropdown-content container when the user clicks on the dropdown button) */ +.show {display:block;} + +.dropdownroleitem { + text-transform: capitalize; + font-size: 12px; + font-weight: bold; + color: white; + background-color: red; + border-color: red; + border-style: solid; + border-width: 1px; + border-radius: 5px; + padding: 3px 6px; + display: inline-block; + margin-top: 5px; + margin-bottom: 5px; + margin-left: 5px; + margin-right: 5px; } \ No newline at end of file