diff --git a/back/.program_data/app.db-shm b/back/.program_data/app.db-shm new file mode 100644 index 0000000..82359f4 Binary files /dev/null and b/back/.program_data/app.db-shm differ diff --git a/back/.program_data/app.db-wal b/back/.program_data/app.db-wal new file mode 100644 index 0000000..cd2476f Binary files /dev/null and b/back/.program_data/app.db-wal differ diff --git a/back/.program_data/imgs/systemkey.lock b/back/.program_data/imgs/systemkey.lock new file mode 100644 index 0000000..967ce29 --- /dev/null +++ b/back/.program_data/imgs/systemkey.lock @@ -0,0 +1 @@ +{"Email":"@system","Key":"caeae1bc-3761-4b30-8627-d86af99b0a4f","Password":"M8I^7b,UF!)PIQ.A"} \ No newline at end of file diff --git a/back/DataModels/EfmigrationsLock.cs b/back/DataModels/EfmigrationsLock.cs new file mode 100644 index 0000000..e9d01bc --- /dev/null +++ b/back/DataModels/EfmigrationsLock.cs @@ -0,0 +1,8 @@ +namespace back.DataModels; + +public partial class EfmigrationsLock +{ + public int Id { get; set; } + + public string Timestamp { get; set; } = null!; +} diff --git a/back/DataModels/Event.cs b/back/DataModels/Event.cs new file mode 100644 index 0000000..81c9219 --- /dev/null +++ b/back/DataModels/Event.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Transactional.Abstractions; + +namespace back.DataModels; + +[Table("Events")] +public partial class Event : ISoftDeletable, IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(50)] + public string Title { get; set; } = null!; + [MaxLength(500)] + public string? Description { get; set; } + public string? Date { get; set; } + public string? Location { get; set; } + public string CreatedAt { get; set; } = null!; + public string UpdatedAt { get; set; } = null!; + public string? CreatedBy { get; set; } + public string? UpdatedBy { get; set; } + public int IsDeleted { get; set; } + public string? DeletedAt { get; set; } + public virtual ICollection Galleries { get; set; } = []; + public virtual ICollection Photos { get; set; } = []; + public virtual ICollection Tags { get; set; } = []; + + public override int GetHashCode() + => HashCode.Combine(Id, Title, Date, Location); + + public override bool Equals(object? obj) + => obj is Event otherEvent && Equals(otherEvent); + + public bool Equals(Event? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id + || (Title == other.Title && Date == other.Date && Location == other.Location) + || GetHashCode() == other.GetHashCode(); + } +} + diff --git a/back/DataModels/EventModel.cs b/back/DataModels/EventModel.cs deleted file mode 100644 index bb2ead8..0000000 --- a/back/DataModels/EventModel.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Events")] -public class EventModel -{ - [Key] - public string Id { get; set; } - [Required, MaxLength(50)] - public string? Title { get; set; } - [MaxLength(500)] - public string? Description { get; set; } - public DateTime? Date { get; set; } - public string? Location { get; set; } - [ForeignKey("TagId")] - public List RelatedTags { get; set; } = []; - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public string? CreatedBy { get; set; } - public string? UpdatedBy { get; set; } - - public EventModel() { } - - public EventModel(string id) - { - Id = id; - } - - public EventModel( - string id, - string title, - string description, - DateTime date, - string location, - List? relatedTags = null, - DateTime? createdAt = null, - DateTime? updatedAt = null, - string? createdBy = null, - string? updatedBy = null) - { - Id = id; - Title = title; - Description = description; - Date = date; - Location = location; - RelatedTags = relatedTags ?? []; - CreatedAt = createdAt ?? DateTime.UtcNow; - UpdatedAt = updatedAt ?? DateTime.UtcNow; - CreatedBy = createdBy ?? ""; - UpdatedBy = updatedBy; - } -} - diff --git a/back/DataModels/Gallery.cs b/back/DataModels/Gallery.cs new file mode 100644 index 0000000..b540d8e --- /dev/null +++ b/back/DataModels/Gallery.cs @@ -0,0 +1,44 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Transactional.Abstractions; + +namespace back.DataModels; + +[Table("Galleries")] +public partial class Gallery: IEquatable, ISoftDeletable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [MaxLength(100)] + public string? Title { get; set; } + [MaxLength(500)] + public string? Description { get; set; } + public string? CreatedAt { get; set; } + public string? UpdatedAt { get; set; } + public string CreatedBy { get; set; } = null!; + public int? IsPublic { get; set; } + public int? IsArchived { get; set; } + public int? IsFavorite { get; set; } + public int IsDeleted { get; set; } + public string? DeletedAt { get; set; } + public string? EventId { get; set; } + public virtual User CreatedByNavigation { get; set; } = null!; + public virtual Event? Event { get; set; } + public virtual ICollection Photos { get; set; } = []; + public virtual ICollection Tags { get; set; } = []; + public virtual ICollection Users { get; set; } = []; + + public Gallery() { } + + public override int GetHashCode() => HashCode.Combine(Id, Title); + + public override bool Equals(object? obj) => obj is Gallery otherEvent && Equals(otherEvent); + + public bool Equals(Gallery? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id || GetHashCode() == other.GetHashCode(); + } +} diff --git a/back/DataModels/GalleryModel.cs b/back/DataModels/GalleryModel.cs deleted file mode 100644 index bb246f2..0000000 --- a/back/DataModels/GalleryModel.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Galleries")] -public class GalleryModel -{ - public GalleryModel() { } - - [Key] - public string Id { get; set; } - [MaxLength(100)] - public string? Title { get; set; } - [MaxLength(500)] - public string? Description { get; set; } - public DateTime? CreatedAt { get; set; } - public DateTime? UpdatedAt { get; set; } - public string? CreatedBy { get; set; } - public string? UpdatedBy { get; set; } - [ForeignKey("PhotoId")] - public List Photos { get; set; } = []; - public bool? IsPublic { get; set; } = true; - public bool? IsArchived { get; set; } = false; - public bool? IsFavorite { get; set; } = false; - public EventModel? Event { get; set; } = null; - public List? Tags { get; set; } = null; - public List? PersonsInvolved { get; set; } = null; - public List? UsersWhoCanSee { get; set; } = null; -} diff --git a/back/DataModels/Permission.cs b/back/DataModels/Permission.cs new file mode 100644 index 0000000..6fa6251 --- /dev/null +++ b/back/DataModels/Permission.cs @@ -0,0 +1,40 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("Permissions")] +public partial class Permission: IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(100)] + public string Name { get; set; } = null!; + [MaxLength(255)] + public string? Description { get; set; } + public virtual ICollection Roles { get; set; } = []; + + public override int GetHashCode() => HashCode.Combine(Id, Name); + + public override bool Equals(object? obj) + => obj is Permission other && Equals(other); + public bool Equals(Permission? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id || GetHashCode() == other.GetHashCode(); + } + + // Static permissions + public static readonly Permission ViewContentPermission = new() { Id = "1", Name = "VIEW_CONTENT", Description = "Permission to view content" }; + public static readonly Permission LikeContentPermission = new() { Id = "2", Name = "LIKE_CONTENT", Description = "Permission to like content" }; + public static readonly Permission EditContentPermission = new() { Id = "3", Name = "EDIT_CONTENT", Description = "Permission to edit content" }; + public static readonly Permission DeleteContentPermission = new() { Id = "4", Name = "DELETE_CONTENT", Description = "Permission to delete content" }; + public static readonly Permission CreateContentPermission = new() { Id = "5", Name = "CREATE_CONTENT", Description = "Permission to create new content" }; + public static readonly Permission EditUserPermission = new() { Id = "6", Name = "EDIT_USER", Description = "Permission to edit user" }; + public static readonly Permission DeleteUserPermission = new() { Id = "7", Name = "DELETE_USER", Description = "Permission to delete user" }; + public static readonly Permission DisableUserPermission = new() { Id = "8", Name = "DISABLE_USER", Description = "Permission to disable user" }; + public static readonly Permission CreateUserPermission = new() { Id = "9", Name = "CREATE_USER", Description = "Permission to create new user" }; + public static readonly Permission EditWebConfigPermission = new() { Id = "10", Name = "EDIT_WEB_CONFIG", Description = "Permission to edit web configuration" }; +} diff --git a/back/DataModels/PermissionModel.cs b/back/DataModels/PermissionModel.cs deleted file mode 100644 index 9c08c43..0000000 --- a/back/DataModels/PermissionModel.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Permissions")] -public class PermissionModel -{ - public PermissionModel() { } - - [Key] - public string Id { get; set; } - [Required, MaxLength(100)] - public string Name { get; set; } - [MaxLength(255)] - public string Description { get; set; } - - public PermissionModel(string id, string name, string description) - { - Id = id; - Name = name; - Description = description; - } - - // Static permissions - public static readonly PermissionModel ViewContentPermission = new("1", "VIEW_CONTENT", "Permission to view content"); - public static readonly PermissionModel LikeContentPermission = new("2", "LIKE_CONTENT", "Permission to like content"); - public static readonly PermissionModel EditContentPermission = new("3", "EDIT_CONTENT", "Permission to edit content"); - public static readonly PermissionModel DeleteContentPermission = new("4", "DELETE_CONTENT", "Permission to delete content"); - public static readonly PermissionModel CreateContentPermission = new("5", "CREATE_CONTENT", "Permission to create new content"); - public static readonly PermissionModel EditUserPermission = new("6", "EDIT_USER", "Permission to edit user"); - public static readonly PermissionModel DeleteUserPermission = new("7", "DELETE_USER", "Permission to delete user"); - public static readonly PermissionModel DisableUserPermission = new("8", "DISABLE_USER", "Permission to disable user"); - public static readonly PermissionModel CreateUserPermission = new("9", "CREATE_USER", "Permission to create new user"); - public static readonly PermissionModel EditWebConfigPermission = new("10", "EDIT_WEB_CONFIG", "Permission to edit web configuration"); -} diff --git a/back/DataModels/Person.cs b/back/DataModels/Person.cs new file mode 100644 index 0000000..1a7a9ac --- /dev/null +++ b/back/DataModels/Person.cs @@ -0,0 +1,51 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using Transactional.Abstractions; + +namespace back.DataModels; + +[Table("Persons")] +public partial class Person: IEquatable, ISoftDeletable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(100)] + public string Name { get; set; } = null!; + public string? ProfilePicture { get; set; } + public string? Avatar { get; set; } + public string? SocialMediaId { get; set; } + [MaxLength(250)] + public string? Bio { get; set; } // Optional field for a short biography or description + public string CreatedAt { get; set; } = null!; + public string? UpdatedAt { get; set; } + public int IsDeleted { get; set; } + public string? DeletedAt { get; set; } + + public virtual ICollection Photos { get; set; } = []; + public virtual SocialMedia? SocialMedia { get; set; } + public virtual User? User { get; set; } + public virtual ICollection PhotosNavigation { get; set; } = []; + + public override int GetHashCode() => HashCode.Combine(Id, Name); + + public override bool Equals(object? obj) + => obj is Person other && Equals(other); + + public bool Equals(Person? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id || GetHashCode() == other.GetHashCode(); + } + + public const string SystemPersonId = "00000000-0000-0000-0000-000000000001"; + + public static readonly Person SystemPerson = new() + { + Id = SystemPersonId, + Name = "System", + CreatedAt = DateTime.UtcNow.ToString("dd-MM-yyyy HH:mm:ss zz"), + User = User.SystemUser + }; +} \ No newline at end of file diff --git a/back/DataModels/PersonModel.cs b/back/DataModels/PersonModel.cs deleted file mode 100644 index 5b95927..0000000 --- a/back/DataModels/PersonModel.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Persons")] -public class PersonModel -{ - public PersonModel() { } - - [Key] - public string Id { get; set; } - [Required, MaxLength(100)] - public string? Name { get; set; } - public string? ProfilePicture { get; set; } - public string? Avatar { get; set; } - [ForeignKey("SocialMediaId")] - public SocialMedia? SocialMedia { get; set; } - [MaxLength(250)] - public string? Bio { get; set; } // Optional field for a short biography or description - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } - - public PersonModel(string id) - { - Id = id; - } - - public PersonModel(string id, string name, string? profilePicture = null, string? avatar = null, SocialMedia? socialMedia = null) - { - Id = id; - Name = name; - ProfilePicture = profilePicture; - Avatar = avatar; - SocialMedia = socialMedia; - } -} - -[Table("SocialMedia")] -public class SocialMedia -{ - [Key] - public string Id { get; set; } = Guid.NewGuid().ToString(); - public string? Facebook { get; set; } - public string? Instagram { get; set; } - public string? Twitter { get; set; } - public string? BlueSky { get; set; } - public string? Tiktok { get; set; } - public string? Linkedin { get; set; } - public string? Pinterest { get; set; } - public string? Discord { get; set; } - public string? Reddit { get; set; } - public string? Other { get; set; } -} diff --git a/back/DataModels/Photo.cs b/back/DataModels/Photo.cs new file mode 100644 index 0000000..aafa032 --- /dev/null +++ b/back/DataModels/Photo.cs @@ -0,0 +1,47 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("Photos")] +public partial class Photo : IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(100), MinLength(1)] + public string Title { get; set; } = null!; + [MaxLength(500)] + public string? Description { get; set; } + public string? Extension { get; set; } + public string? LowResUrl { get; set; } + public string? MidResUrl { get; set; } + public string? HighResUrl { get; set; } + public string? CreatedAt { get; set; } + public string? UpdatedAt { get; set; } + public string CreatedBy { get; set; } = null!; + public string? UpdatedBy { get; set; } + public string? EventId { get; set; } + public string? RankingId { get; set; } + public int? IsFavorite { get; set; } + public int? IsPublic { get; set; } + public int? IsArchived { get; set; } + public virtual Person CreatedByNavigation { get; set; } = null!; + public virtual Event? Event { get; set; } + public virtual ICollection Galleries { get; set; } = []; + public virtual ICollection People { get; set; } = []; + public virtual ICollection Tags { get; set; } = []; + public virtual ICollection Users { get; set; } = []; + + public override int GetHashCode() => HashCode.Combine(Id, Title); + + public override bool Equals(object? obj) + => obj is Photo otherEvent && Equals(otherEvent); + + public bool Equals(Photo? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id || GetHashCode() == other.GetHashCode(); + } +} \ No newline at end of file diff --git a/back/DataModels/PhotoModel.cs b/back/DataModels/PhotoModel.cs deleted file mode 100644 index d52c613..0000000 --- a/back/DataModels/PhotoModel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Photos")] -public class PhotoModel -{ - public PhotoModel() { } - - [Key] - public string Id { get; set; } - [Required, MaxLength(100), MinLength(1)] - public string Title { get; set; } - [MaxLength(500)] - public string Description { get; set; } - public string Extension { get; set; } - public string LowResUrl { get; set; } - public string MidResUrl { get; set; } - public string HighResUrl { get; set; } - public DateTime CreatedAt { get; set; } - public DateTime UpdatedAt { get; set; } - public string CreatedBy { get; set; } - public string UpdatedBy { get; set; } - [ForeignKey("EventId")] - public EventModel? Event { get; set; } = null; - [ForeignKey("TagId")] - public List Tags { get; set; } = []; - [ForeignKey("RankingId")] - public RankingModel Ranking { get; set; } = new RankingModel(0); - public bool IsFavorite { get; set; } = false; - public bool IsPublic { get; set; } = true; - public bool IsArchived { get; set; } = false; - [ForeignKey("PersonId")] - public List? Persons { get; set; } - - public PhotoModel( - string id, - string title, - string description, - string lowResUrl, - string midResUrl, - string highResUrl, - DateTime createdAt, - DateTime updatedAt, - string createdBy, - string updatedBy, - EventModel? @event = null, - List? tags = null, - RankingModel? ranking = null, - bool isFavorite = false, - bool isPublic = true, - bool isArchived = false, - List? persons = null) - { - Id = id; - Title = title; - Description = description; - LowResUrl = lowResUrl; - MidResUrl = midResUrl; - HighResUrl = highResUrl; - CreatedAt = createdAt; - UpdatedAt = updatedAt; - CreatedBy = createdBy; - UpdatedBy = updatedBy; - Event = @event ?? Event; - Tags = tags ?? Tags; - Ranking = ranking ?? Ranking; - IsFavorite = isFavorite; - IsPublic = isPublic; - IsArchived = isArchived; - Persons = persons ?? Persons; - } -} \ No newline at end of file diff --git a/back/DataModels/Ranking.cs b/back/DataModels/Ranking.cs new file mode 100644 index 0000000..408e945 --- /dev/null +++ b/back/DataModels/Ranking.cs @@ -0,0 +1,119 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +public class RankingGroup +{ + public const float ExtremlyBad = 1/5f; + public const float Bad = 2 / 5f; + public const float Normal = 3 / 5f; + public const float Good = 4 / 5f; + public const float ExtremlyGood = 5 / 5f; + + public static string GetGroup(float score) => (float)Math.Ceiling(score) switch + { + <= ExtremlyBad => nameof(ExtremlyBad), + <= Bad => nameof(Bad), + <= Normal => nameof(Normal), + <= Good => nameof(Good), + _ => nameof(ExtremlyGood) + }; +} + +[Table("Rankings")] +public partial class Ranking : IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + public int TotalVotes { get; set; } + public int UpVotes { get; set; } + public int DownVotes { get; set; } + + public Ranking(int totalVotes, int upVotes = 0, int downVotes = 0) + { + TotalVotes = totalVotes; + UpVotes = upVotes; + DownVotes = downVotes; + } + + public Ranking() + { + TotalVotes = 0; + UpVotes = 0; + DownVotes = 0; + } + + public void DownVote() + { + DownVotes++; + TotalVotes++; + } + + public void UpVote() + { + UpVotes++; + TotalVotes++; + } + + public float Score + { + get + { + if (TotalVotes == 0) return 0; + return (float)(UpVotes - DownVotes) / TotalVotes; + } + } + + public string Group => RankingGroup.GetGroup(Score); + + public override int GetHashCode() => HashCode.Combine(Id, TotalVotes, UpVotes, DownVotes); + + public override bool Equals(object? obj) => obj is Ranking otherEvent && Equals(otherEvent); + + public bool Equals(Ranking? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return + Id == other.Id + || GetHashCode() == other.GetHashCode() + || (TotalVotes == other.TotalVotes && UpVotes == other.UpVotes && DownVotes == other.DownVotes); + } + + public static bool operator ==(Ranking ranking1, Ranking ranking2) + { + if (ranking1 is null && ranking2 is null) return true; + if (ranking1 is null || ranking2 is null) return false; + return ranking1.Equals(ranking2); + } + + public static bool operator !=(Ranking ranking1, Ranking ranking2) + { + if (ranking1 is null && ranking2 is null) return false; + if (ranking1 is null || ranking2 is null) return true; + return !ranking1.Equals(ranking2); + } + + public static bool operator < (Ranking ranking1, Ranking ranking2) + { + ArgumentNullException.ThrowIfNull(ranking1, nameof(ranking1)); + ArgumentNullException.ThrowIfNull(ranking2, nameof(ranking2)); + return ranking1.Score < ranking2.Score; + } + + public static bool operator > (Ranking ranking1, Ranking ranking2) + { + ArgumentNullException.ThrowIfNull(ranking1, nameof(ranking1)); + ArgumentNullException.ThrowIfNull(ranking2, nameof(ranking2)); + if (ranking1 is null && ranking2 is null) return true; + if (ranking1 is null || ranking2 is null) return false; + return ranking1.Score > ranking2.Score; + } + + public static bool operator <= (Ranking ranking1, Ranking ranking2) + => ranking1 == ranking2 || ranking1 < ranking2; + + public static bool operator >= (Ranking ranking1, Ranking ranking2) + => ranking1 == ranking2 || ranking1 > ranking2; +} diff --git a/back/DataModels/RankingModel.cs b/back/DataModels/RankingModel.cs deleted file mode 100644 index b37b312..0000000 --- a/back/DataModels/RankingModel.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Rankings")] -public class RankingModel -{ - [Key] - public string Id { get; set; } = Guid.NewGuid().ToString(); - - private int totalVotes; - private int upVotes; - private int downVotes; - - public RankingModel(int totalVotes, int upVotes = 0, int downVotes = 0) - { - this.totalVotes = totalVotes; - this.upVotes = upVotes; - this.downVotes = downVotes; - } - - public RankingModel() - { - totalVotes = 0; - upVotes = 0; - downVotes = 0; - } - - public void DownVote() - { - downVotes++; - totalVotes++; - } - - public void UpVote() - { - upVotes++; - totalVotes++; - } - - public double Score - { - get - { - if (totalVotes == 0) return 0; - return (double)(upVotes - downVotes) / totalVotes; - } - } -} diff --git a/back/DataModels/Role.cs b/back/DataModels/Role.cs new file mode 100644 index 0000000..a0466ad --- /dev/null +++ b/back/DataModels/Role.cs @@ -0,0 +1,94 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("Roles")] +public partial class Role : IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(100)] + public string Name { get; set; } = null!; + [MaxLength(250)] + public string? Description { get; set; } + public string? BaseRoleModelId { get; set; } + public virtual Role? BaseRoleModel { get; set; } + public virtual ICollection InverseBaseRoleModel { get; set; } = []; + public virtual ICollection Permissions { get; set; } = new HashSet(); + public virtual ICollection Users { get; set; } = []; + + + public bool IsAdmin() => BaseRoleModel != null ? BaseRoleModel.IsAdmin() : Id == AdminRole.Id; + public bool IsContentManager() => BaseRoleModel != null ? BaseRoleModel.IsContentManager() : Id == ContentManagerRole.Id; + public bool IsUser() => BaseRoleModel != null ? BaseRoleModel.IsUser() : Id == UserRole.Id; + + public bool HasPermission(Permission permission) + { + var baseRoleHasPermission = BaseRoleModel != null && BaseRoleModel.HasPermission(permission); + return baseRoleHasPermission || Permissions.Any(p => p.Id == permission.Id); + } + + public Role() { } + + public Role(string id, string name, string description, List? permissions = null, Role? baseRoleModel = null) + { + Id = id; + Name = name; + Description = description; + Permissions = permissions ?? []; + if (baseRoleModel != null) + { + BaseRoleModel = baseRoleModel; + BaseRoleModelId = baseRoleModel.Id; + foreach (var permission in baseRoleModel.Permissions) + { + if (!Permissions.Any(p => p.Id == permission.Id)) + { + Permissions.Add(permission); + } + } + } + } + + public override int GetHashCode() => HashCode.Combine(Id, Name); + + public override bool Equals(object? obj) => obj is Role otherEvent && Equals(otherEvent); + + public bool Equals(Role? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id || GetHashCode() == other.GetHashCode(); + } + + public static readonly Role UserRole = new( + "1", "User", "Role for regular users", + [ + Permission.ViewContentPermission, + Permission.LikeContentPermission + ] + ); + + public static readonly Role ContentManagerRole = new( + "2", "Content Manager", "Role for managing content", + [ + Permission.CreateContentPermission, + Permission.EditContentPermission, + Permission.DeleteContentPermission + ], + UserRole + ); + + public static readonly Role AdminRole = new( + "3", "Admin", "Administrator role with full permissions", + [ + Permission.CreateUserPermission, + Permission.DisableUserPermission, + Permission.EditUserPermission, + Permission.DeleteUserPermission, + Permission.EditWebConfigPermission + ], + ContentManagerRole + ); +} diff --git a/back/DataModels/RoleModel.cs b/back/DataModels/RoleModel.cs deleted file mode 100644 index bc1d97a..0000000 --- a/back/DataModels/RoleModel.cs +++ /dev/null @@ -1,74 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Tags("Roles")] -public class RoleModel -{ - public RoleModel() { } - - [Key] - public string Id { get; set; } - [Required, MaxLength(100)] - public string Name { get; set; } - [MaxLength(250)] - public string Description { get; set; } - [ForeignKey("PermissionId")] - public List Permissions { get; set; } - [ForeignKey("RoleId")] - public RoleModel? BaseRoleModel { get; set; } - - public RoleModel(string id, string name, string description, List? permissions = null, RoleModel? baseRoleModel = null) - { - Id = id; - Name = name; - Description = description; - Permissions = permissions ?? new List(); - BaseRoleModel = baseRoleModel; - if (baseRoleModel != null) - { - Permissions.AddRange(baseRoleModel.Permissions); - } - } - - public bool IsAdmin => Id == AdminRole.Id; - public bool IsContentManager => Id == ContentManagerRole.Id; - public bool IsUser => Id == UserRole.Id; - - public bool HasPermission(PermissionModel permission) - { - return Permissions.Exists(p => p.Id == permission.Id); - } - - public static readonly RoleModel UserRole = new( - "1", "User", "Role for regular users", - new List { - PermissionModel.ViewContentPermission, - PermissionModel.LikeContentPermission - } - ); - - public static readonly RoleModel ContentManagerRole = new( - "2", "Content Manager", "Role for managing content", - new List { - PermissionModel.CreateUserPermission, - PermissionModel.DisableUserPermission, - PermissionModel.CreateContentPermission, - PermissionModel.EditContentPermission, - PermissionModel.DeleteContentPermission - }, - UserRole - ); - - public static readonly RoleModel AdminRole = new( - "3", "Admin", "Administrator role with full permissions", - new List { - PermissionModel.CreateUserPermission, - PermissionModel.EditUserPermission, - PermissionModel.DeleteUserPermission, - PermissionModel.EditWebConfigPermission - }, - ContentManagerRole - ); -} diff --git a/back/DataModels/SocialMedia.cs b/back/DataModels/SocialMedia.cs new file mode 100644 index 0000000..009a05c --- /dev/null +++ b/back/DataModels/SocialMedia.cs @@ -0,0 +1,33 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("SocialMedia")] +public partial class SocialMedia: IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + public string? Facebook { get; set; } + public string? Instagram { get; set; } + public string? Twitter { get; set; } + public string? BlueSky { get; set; } + public string? Tiktok { get; set; } + public string? Linkedin { get; set; } + public string? Pinterest { get; set; } + public string? Discord { get; set; } + public string? Reddit { get; set; } + public string? Other { get; set; } + public virtual ICollection People { get; set; } = []; + + public override int GetHashCode() => HashCode.Combine(Id); + + public override bool Equals(object? obj) => obj is SocialMedia otherEvent && Equals(otherEvent); + + public bool Equals(SocialMedia? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return GetHashCode() == other.GetHashCode(); + } +} diff --git a/back/DataModels/SystemKey.cs b/back/DataModels/SystemKey.cs new file mode 100644 index 0000000..22fd910 --- /dev/null +++ b/back/DataModels/SystemKey.cs @@ -0,0 +1,15 @@ +namespace back.DataModels; + +public class SystemKey +{ + public string Email { get; set; } = "@system"; + public string Key { get; set; } = Guid.NewGuid().ToString(); + public required string Password { get; set; } + + public bool IsValid(string email, string password, string key) + { + return Email.Equals(email, StringComparison.InvariantCultureIgnoreCase) && + Password.Equals(password, StringComparison.InvariantCulture) && + Key.Equals(key, StringComparison.InvariantCulture); + } +} \ No newline at end of file diff --git a/back/DataModels/Tag.cs b/back/DataModels/Tag.cs new file mode 100644 index 0000000..94565ee --- /dev/null +++ b/back/DataModels/Tag.cs @@ -0,0 +1,32 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("Tags")] +public partial class Tag: IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, MaxLength(25)] + public string Name { get; set; } = null!; + [Required] + public string CreatedAt { get; set; } = null!; + + public virtual ICollection Events { get; set; } = []; + + public virtual ICollection Galleries { get; set; } = []; + + public virtual ICollection Photos { get; set; } = []; + + public override int GetHashCode() => HashCode.Combine(Id, Name); + + public override bool Equals(object? obj) => obj is Tag tag && Equals(tag); + + public bool Equals(Tag? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id || GetHashCode() == other.GetHashCode(); + } +} diff --git a/back/DataModels/TagModel.cs b/back/DataModels/TagModel.cs deleted file mode 100644 index 211c8f3..0000000 --- a/back/DataModels/TagModel.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Tags")] -public class TagModel -{ - public TagModel() { } - public TagModel(string id, string? name = null) - { - Id = id; - Name = name; - } - - [Key] - public string Id { get; init; } - [Required, MaxLength(25)] - public string? Name { get; init; } - public DateTime CreatedAt { get; init; } = DateTime.UtcNow; -} diff --git a/back/DataModels/User.cs b/back/DataModels/User.cs new file mode 100644 index 0000000..a099e2b --- /dev/null +++ b/back/DataModels/User.cs @@ -0,0 +1,57 @@ +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; + +namespace back.DataModels; + +[Table("Users")] +public class User : IEquatable +{ + [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] + public string Id { get; set; } = null!; + [Required, EmailAddress] + public string Email { get; set; } = null!; + [Required, MinLength(8)] + public string Password { get; set; } = null!; + [Required] + public string Salt { get; set; } = null!; + public string CreatedAt { get; set; } = null!; + + public virtual Person IdNavigation { get; set; } = null!; + public virtual ICollection Galleries { get; set; } = []; + public virtual ICollection GalleriesNavigation { get; set; } = []; + public virtual ICollection Photos { get; set; } = []; + public virtual ICollection Roles { get; set; } = []; + + public User() { } + public User(string id, string email, string password, DateTimeOffset createdAt) + { + Id = id; + Email = email; + Password = password; + CreatedAt = createdAt.ToString("dd-MM-yyyy HH:mm:ss zz"); + } + + public bool IsAdmin() => Roles.Any(r => r.IsAdmin()); + public bool IsContentManager() => Roles.Any(r => r.IsContentManager()); + public bool IsUser() => Roles.Any(r => r.IsUser()); + + public override int GetHashCode() => HashCode.Combine(Id, Email); + + public override bool Equals(object? obj) => obj is User otherEvent && Equals(otherEvent); + + public bool Equals(User? other) + { + if (other is null) return false; + if (ReferenceEquals(this, other)) return true; + return Id == other.Id && Email == other.Email; + } + + public const string SystemUserId = "00000000-0000-0000-0000-000000000001"; + + public static readonly User SystemUser = new( + id: SystemUserId, + email: "@system", + password: "", + createdAt: DateTime.UtcNow + ); +} \ No newline at end of file diff --git a/back/DataModels/UserModel.cs b/back/DataModels/UserModel.cs deleted file mode 100644 index 1371e1b..0000000 --- a/back/DataModels/UserModel.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace back.DataModels; - -[Table("Users")] -public class UserModel : PersonModel -{ - [Required] - public string Email { get; set; } - [Required, MinLength(8)] - public string Password { get; set; } - [Required] - public string Salt { get; set; } - [ForeignKey("RoleId")] - public List Role { get; set; } - - public UserModel(string id, string email, string password, string name, List role, DateTime createdAt, DateTime updatedAt) - : base(id, name) - { - Email = email; - Password = password; - Role = role; - CreatedAt = createdAt; - UpdatedAt = updatedAt; - } - - public UserModel() { } - - public bool IsAdmin => Role.Exists(r => r.IsAdmin); - public bool IsContentManager => Role.Exists(r => r.IsContentManager); - public bool IsUser => Role.Exists(r => r.IsUser); - - public UserDto ToDto() - { - return new UserDto - { - Id = Id, - Name = Name, - Email = Email, - ProfilePicture = ProfilePicture, - Avatar = Avatar, - SocialMedia = SocialMedia, - Bio = Bio, - CreatedAt = CreatedAt, - UpdatedAt = UpdatedAt - }; - } -} - -public class UserDto : PersonModel -{ - public required string Email { get; set; } -} \ No newline at end of file diff --git a/back/Options/MailServerOptions.cs b/back/Options/MailServerOptions.cs new file mode 100644 index 0000000..f8f74fc --- /dev/null +++ b/back/Options/MailServerOptions.cs @@ -0,0 +1,10 @@ +namespace back.Options; + +public sealed class MailServerOptions +{ + public required string SmtpServer { get; set; } + public required int Puerto { get; set; } + public required string Usuario { get; set; } + public required string Password { get; set; } + public bool EnableSsl { get; set; } +} diff --git a/back/Program.cs b/back/Program.cs index 939dd89..7440428 100644 --- a/back/Program.cs +++ b/back/Program.cs @@ -9,7 +9,6 @@ public class Program var builder = WebApplication.CreateBuilder(args); builder.Services.UseExtensions(); - builder.Services.AddMemoryCache(); builder.Services.AddControllers(); // Learn more about configuring OpenAPI at https://aka.ms/aspnet/openapi diff --git a/back/ServicesExtensions/DatabaseContexts.cs b/back/ServicesExtensions/DatabaseContexts.cs index be93b51..c982e3d 100644 --- a/back/ServicesExtensions/DatabaseContexts.cs +++ b/back/ServicesExtensions/DatabaseContexts.cs @@ -1,5 +1,5 @@ -using back.context; using back.Options; +using back.persistance.data; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Options; @@ -7,16 +7,9 @@ namespace back.ServicesExtensions; public static partial class ServicesExtensions { - private static IServiceCollection AddDatabaseContexts(this IServiceCollection services) + private static IServiceCollection AddDatabaseContext(this IServiceCollection services) { - services - .AddContext() - .AddContext() - .AddContext() - .AddContext() - .AddContext() - .AddContext() - ; + services.AddContext(); return services; } @@ -32,6 +25,25 @@ public static partial class ServicesExtensions { options.UseDatabaseConfig(config); }); + + using var scope = services.BuildServiceProvider().CreateScope(); + var context = scope.ServiceProvider + .GetRequiredService(); + var isDevelopment = scope.ServiceProvider + .GetRequiredService() + .IsDevelopment(); + + if (isDevelopment && !context.Database.HasPendingModelChanges()) + { + context.Database.EnsureCreated(); + } + else + { + context.Database.EnsureCreated(); + context.Database.Migrate(); + } + context.SaveChanges(); + return services; } } diff --git a/back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs b/back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs index 5f747c9..367ad22 100644 --- a/back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs +++ b/back/ServicesExtensions/DdContextOptionsBuilderExtensions.cs @@ -1,5 +1,6 @@ using back.Options; using Microsoft.EntityFrameworkCore; +using System.Text.RegularExpressions; namespace back.ServicesExtensions; @@ -59,6 +60,14 @@ public static partial class DbContextOptionsBuilderExtensions switch (provider) { case DatabaseProvider.Sqlite: + var match = SQLiteRegex().Match(config.ConnectionString ?? string.Empty); + if (match.Success) + { + string? folder = null; + string path = match.Groups[1].Value.Replace("\\", "/"); + folder = path.Contains('/') ? path[..path.IndexOf('/')] : path; + Directory.CreateDirectory(folder); + } options.UseSqlite(config.ConnectionString); break; case DatabaseProvider.InMemory: @@ -77,4 +86,7 @@ public static partial class DbContextOptionsBuilderExtensions throw new InvalidOperationException($"Unsupported database provider: {config.Provider}"); } } + + [GeneratedRegex(@"Data Source=([^;]+)")] + private static partial Regex SQLiteRegex(); } diff --git a/back/ServicesExtensions/Options.cs b/back/ServicesExtensions/Options.cs index e0cc5c9..7ee253a 100644 --- a/back/ServicesExtensions/Options.cs +++ b/back/ServicesExtensions/Options.cs @@ -12,10 +12,11 @@ public static partial class ServicesExtensions services.Configure(config.GetSection(nameof(Databases))); services.Configure(DatabaseConfig.DataStorage, config.GetSection(DatabaseConfig.DataStorage)); services.Configure(DatabaseConfig.BlobStorage, config.GetSection(DatabaseConfig.BlobStorage)); + services.Configure(config.GetSection(nameof(MailServerOptions))); services.PostConfigure(databases => { - if (databases.BaseDirectory != null && !Directory.Exists(databases.BaseDirectory)) + if (!string.IsNullOrEmpty(databases.BaseDirectory) && !Directory.Exists(databases.BaseDirectory)) { try { diff --git a/back/ServicesExtensions/ServicesExtensions.cs b/back/ServicesExtensions/ServicesExtensions.cs index 3abacc2..5a78655 100644 --- a/back/ServicesExtensions/ServicesExtensions.cs +++ b/back/ServicesExtensions/ServicesExtensions.cs @@ -1,6 +1,10 @@ -using back.persistance.blob; -using back.services.Crypto; -using back.services.ImageResizer; +using back.persistance.data; +using back.persistance.data.repositories; +using back.persistance.data.repositories.Abstracts; +using back.services.engine.SystemUser; +using DependencyInjector; +using Transactional.Abstractions.Interfaces; +using Transactional.Implementations.EntityFramework; namespace back.ServicesExtensions; @@ -8,15 +12,20 @@ public static partial class ServicesExtensions { public static IServiceCollection UseExtensions(this IServiceCollection services) { - var config = services.ConfigureOptions(); + //var config = + services.ConfigureOptions(); - services.AddDatabaseContexts(); + services.AddMemoryCache(); - // TODO: Move and configure for using S3, Azure Blob Storage, etc. - services.AddSingleton(); + services.AddDatabaseContext(); + services.AddServices(); + + services.AddScoped, EntityFrameworkTransactionalService>(); + + using var scope = services.BuildServiceProvider().CreateScope(); + scope.ServiceProvider + .GetRequiredService().GenerateAsync().Wait(); - services.AddSingleton(); - services.AddSingleton(); return services; } diff --git a/back/appsettings.Development.json b/back/appsettings.Development.json index 274a61a..0868ace 100644 --- a/back/appsettings.Development.json +++ b/back/appsettings.Development.json @@ -1,14 +1,21 @@ { "Databases": { - "BaseDirectory": "data", + "BaseDirectory": ".program_data", "Data": { "Provider": "sqlite", - "ConnectionString": "Data Source=data/app.db;Cache=Shared" + "ConnectionString": "Data Source=.program_data/app.db" }, "Blob": { "Provider": "system", "baseUrl": "https://localhost:7273/api/photo/{id}/{res}", "SystemContainer": "imgs" } + }, + "MailServerOptions": { + "SmtpServer": "smtp.gmail.com", + "Puerto": 587, + "Usuario": "", + "Password": "", + "EnableSsl": true } } diff --git a/back/back.csproj b/back/back.csproj index d008215..9e2a58f 100644 --- a/back/back.csproj +++ b/back/back.csproj @@ -8,6 +8,7 @@ + @@ -42,7 +43,8 @@ - + + diff --git a/back/back.sln b/back/back.sln index a34ebb1..80d938b 100644 --- a/back/back.sln +++ b/back/back.sln @@ -1,10 +1,14 @@  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 -VisualStudioVersion = 17.14.36401.2 d17.14 +VisualStudioVersion = 17.14.36401.2 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "back", "back.csproj", "{392278F3-4B36-47F4-AD31-5FBFCC181AD4}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Transactional", "..\..\nuget\Transactional\Transactional.csproj", "{ED76105A-5E6F-4997-86FE-6A7902A2AEBA}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DependencyInjector", "..\..\nuget\DependencyInjector\DependencyInjector.csproj", "{DBDF84A4-235C-4F29-8626-5BD1DC255970}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +19,14 @@ Global {392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Debug|Any CPU.Build.0 = Debug|Any CPU {392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Release|Any CPU.ActiveCfg = Release|Any CPU {392278F3-4B36-47F4-AD31-5FBFCC181AD4}.Release|Any CPU.Build.0 = Release|Any CPU + {ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {ED76105A-5E6F-4997-86FE-6A7902A2AEBA}.Release|Any CPU.Build.0 = Release|Any CPU + {DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {DBDF84A4-235C-4F29-8626-5BD1DC255970}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.ActiveCfg = Release|Any CPU + {DBDF84A4-235C-4F29-8626-5BD1DC255970}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/back/context/EventContext.cs b/back/context/EventContext.cs deleted file mode 100644 index 605ab9a..0000000 --- a/back/context/EventContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using back.DataModels; -using Microsoft.EntityFrameworkCore; - -namespace back.context; - -public class EventContext : DbContext -{ - private DbSet Events { get; set; } - public EventContext(DbContextOptions options) : base(options) - { - Database.EnsureCreated(); - } - - public async Task GetById(string id) - { - return await GetById(Guid.Parse(id)); - } - - public async Task GetById(Guid id) - { - return await Events.FindAsync(id); - } - - public async Task GetTotalItems() - { - return await Events.CountAsync(); - } - - public async Task> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - - return await Events - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - - public async Task Exists(EventModel? photo) - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Events.AnyAsync(p => p.Id == photo.Id); - } - - public async Task Exists(string id) - { - return await Events.AnyAsync(p => p.Id == id); - } -} diff --git a/back/context/GalleryContext.cs b/back/context/GalleryContext.cs deleted file mode 100644 index b0127c0..0000000 --- a/back/context/GalleryContext.cs +++ /dev/null @@ -1,48 +0,0 @@ -using back.DataModels; -using Microsoft.EntityFrameworkCore; - -namespace back.context; - -public class GalleryContext : DbContext -{ - public DbSet Galleries { get; set; } - public GalleryContext(DbContextOptions options) : base(options) - { - // Ensure database is created - Database.EnsureCreated(); - } - - public async Task GetById(Guid id) - { - return await Galleries.FindAsync(id); - } - - public async Task GetTotalItems() - { - return await Galleries.CountAsync(); - } - - public async Task> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - - return await Galleries - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - - public async Task Exists(GalleryModel? photo) - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Galleries.AnyAsync(p => p.Id == photo.Id); - } - - public async Task Exists(string id) - { - return await Galleries.AnyAsync(p => p.Id == id); - } -} \ No newline at end of file diff --git a/back/context/PersonContext.cs b/back/context/PersonContext.cs deleted file mode 100644 index c961eff..0000000 --- a/back/context/PersonContext.cs +++ /dev/null @@ -1,53 +0,0 @@ -using back.DataModels; -using Microsoft.EntityFrameworkCore; - -namespace back.context; - -public class PersonContext : DbContext -{ - private DbSet Persons { get; set; } - public PersonContext(DbContextOptions options) : base(options) - { - // Ensure database is created - Database.EnsureCreated(); - } - - public async Task GetById(string id) - { - return await GetById(Guid.Parse(id)); - } - - public async Task GetById(Guid id) - { - return await Persons.FindAsync(id); - } - - public async Task GetTotalItems() - { - return await Persons.CountAsync(); - } - - public async Task> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - - return await Persons - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - - public async Task Exists(PersonModel? photo) - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Persons.AnyAsync(p => p.Id == photo.Id); - } - - public async Task Exists(string id) - { - return await Persons.AnyAsync(p => p.Id == id); - } -} diff --git a/back/context/PhotoContext.cs b/back/context/PhotoContext.cs deleted file mode 100644 index 3249afd..0000000 --- a/back/context/PhotoContext.cs +++ /dev/null @@ -1,270 +0,0 @@ -using back.DataModels; -using back.DTO; -using back.persistance.blob; -using back.services.ImageResizer; -using Microsoft.EntityFrameworkCore; - -namespace back.context; - -public class PhotoContext : DbContext -{ - private DbSet Photos { get; set; } - private readonly IImageResizer _Resizer; - private readonly IBlobStorageService _BlobStorage; - - private readonly TagContext _tagContext; - private readonly EventContext _eventContext; - private readonly PersonContext _personContext; - - public PhotoContext(DbContextOptions options, - IImageResizer resizer, - IBlobStorageService blobStorage, - TagContext tags, - EventContext events, - PersonContext persons - ) : base(options) - { - // Ensure database is created - Database.EnsureCreated(); - _Resizer = resizer; - _BlobStorage = blobStorage; - _tagContext = tags; - _eventContext = events; - _personContext = persons; - } - - public async Task CreateNew(PhotoFormModel? form) - { - if (form == null) { return; } - - var photo = new PhotoModel( - Guid.NewGuid().ToString(), - form.Title, - form.Description ?? string.Empty, - string.Empty, // LowResUrl will be set later - string.Empty, // MidResUrl will be set later - string.Empty, // HighResUrl will be set later - DateTime.UtcNow, - DateTime.UtcNow, - form.UserId, - form.UserId - ) - { - IsPublic = form.IsPublic - }; - - List tasks = [ - SaveBlob(photo, form), - LinkTags(photo, form.Tags ?? [], form.UserId), - LinkEvent(photo, form.Evento ?? "", form.UserId), - LinkPersons(photo, form.People ?? [], form.UserId), - ]; - - await Task.WhenAll(tasks); - await Photos.AddAsync(photo); - await SaveChangesAsync(); - } - - private async Task LinkPersons(PhotoModel photo, string[] personas, string updatedBy = "SYSTEM") - { - if (photo == null || personas == null || personas.Length == 0) return; - foreach (var personId in personas) - { - var person = await _personContext.GetById(personId); - if (person != null) - { - await LinkPersons(photo, person, updatedBy); - } - } - } - - private async Task LinkPersons(PhotoModel photo, PersonModel tag, string updatedBy = "SYSTEM") - { - if (tag == null) return; - // Ensure the tag exists - if (await _personContext.Exists(tag.Id)) - { - photo.Persons ??= []; - photo.Persons.Add(tag); - photo.UpdatedAt = DateTime.UtcNow; - photo.UpdatedBy = updatedBy; // or use a more appropriate value - } - } - - private async Task LinkTags(PhotoModel photo, string[] tags, string updatedBy = "SYSTEM") - { - if (photo == null || tags == null || tags.Length == 0) return; - foreach (var tagId in tags) - { - var tag = await _tagContext.GetById(tagId); - if (tag != null) - { - await LinkTag(photo, tag, updatedBy); - } - } - } - - private async Task LinkTag(PhotoModel photo, TagModel tag, string updatedBy = "SYSTEM") - { - if (tag == null) return; - // Ensure the tag exists - if (await _tagContext.Exists(tag.Id)) - { - photo.Tags.Add(tag); - photo.UpdatedAt = DateTime.UtcNow; - photo.UpdatedBy = updatedBy; // or use a more appropriate value - } - } - - private async Task LinkEvent(PhotoModel photo, string eventId, string updatedBy = "SYSTEM") - { - if (string.IsNullOrEmpty(eventId)) return; - var evento = await _eventContext.GetById(eventId); - if (evento != null) - { - await LinkEvent(photo, evento, updatedBy); - } - } - - private async Task LinkEvent(PhotoModel photo, EventModel? evento, string updatedBy = "SYSTEM") - { - if (evento == null) return; - // Ensure the event exists - if (await _eventContext.Exists(evento.Id)) - { - photo.Event = evento; - photo.UpdatedAt = DateTime.UtcNow; - photo.UpdatedBy = updatedBy; - } - } - - private async Task SaveBlob(PhotoModel photo, PhotoFormModel form) - { - if (form.Image != null && form.Image.Length > 0) - { - var lowRes = await _Resizer.ResizeImage(form.Image, 480); - var midRes = await _Resizer.ResizeImage(form.Image, 720); - // Upload images to blob storage - photo.Extension = form.Image.FileName.Split('.').Last(); - photo.LowResUrl = $"low/{photo.Id}.webp"; - photo.MidResUrl = $"mid/{photo.Id}.webp"; - photo.HighResUrl = $"high/{photo.Id}.{photo.Extension}"; - await _BlobStorage.SaveAsync(lowRes, photo.LowResUrl); - await _BlobStorage.SaveAsync(midRes, photo.MidResUrl); - await _BlobStorage.SaveAsync(form.Image.OpenReadStream(), photo.HighResUrl); - } - } - - public async Task GetById(string id) - { - return await GetById(Guid.Parse(id)); - } - - public async Task GetById(Guid id) - { - try - { - return await Photos.FindAsync(id); - } - catch - { - return null; - } - } - - public async Task GetTotalItems() - { - try - { - return await Photos.CountAsync(); - } - catch - { - return 0; - } - } - - public async Task?> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - try - { - return await Photos - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - catch - { - return null; - } - } - - public async Task Exists(PhotoModel? photo) - { - try - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Photos.AnyAsync(p => p.Id == photo.Id); - } - catch - { - return false; // Handle exceptions gracefully - } - } - - public async Task Exists(string id) - { - try - { - if (string.IsNullOrEmpty(id)) return false; - return await Photos.AnyAsync(p => p.Id == id); - } - catch - { - return false; // Handle exceptions gracefully - } - } - - public async Task Delete(PhotoModel photo) - { - if (photo == null) return; - if (await Exists(photo)) - { - // Delete the photo from blob storage - if (!string.IsNullOrEmpty(photo.LowResUrl)) - await _BlobStorage.DeleteAsync(photo.LowResUrl); - if (!string.IsNullOrEmpty(photo.MidResUrl)) - await _BlobStorage.DeleteAsync(photo.MidResUrl); - if (!string.IsNullOrEmpty(photo.HighResUrl)) - await _BlobStorage.DeleteAsync(photo.HighResUrl); - Photos.Remove(photo); - await SaveChangesAsync(); - } - } - - public async Task Update(PhotoModel photo) - { - if (photo == null) return; - if (await Exists(photo)) - { - var evento = photo.Event; - photo.Event = null; - await LinkEvent(photo, evento, photo.UpdatedBy); - - var tags = photo.Tags.Select(t => t.Id); - photo.Tags.Clear(); - await LinkTags(photo, [.. tags], photo.UpdatedBy); - - var persons = photo.Persons?.Select(t => t.Id) ?? []; - photo.Persons = null; - await LinkPersons(photo, [.. persons], photo.UpdatedBy); - - Photos.Update(photo); - await SaveChangesAsync(); - } - } -} diff --git a/back/context/TagContext.cs b/back/context/TagContext.cs deleted file mode 100644 index e974484..0000000 --- a/back/context/TagContext.cs +++ /dev/null @@ -1,52 +0,0 @@ -using back.DataModels; -using Microsoft.EntityFrameworkCore; - -namespace back.context; - -public class TagContext : DbContext -{ - private DbSet Tags { get; set; } - public TagContext(DbContextOptions options) : base(options) - { - Database.EnsureCreated(); - } - - public async Task GetById(string id) - { - return await GetById(Guid.Parse(id)); - } - - public async Task GetById(Guid id) - { - return await Tags.FindAsync(id); - } - - public async Task GetTotalItems() - { - return await Tags.CountAsync(); - } - - public async Task> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - - return await Tags - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - - public async Task Exists(TagModel? photo) - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Tags.AnyAsync(p => p.Id == photo.Id); - } - - public async Task Exists(string id) - { - return await Tags.AnyAsync(p => p.Id == id); - } -} diff --git a/back/context/UserContext.cs b/back/context/UserContext.cs deleted file mode 100644 index 2754c5c..0000000 --- a/back/context/UserContext.cs +++ /dev/null @@ -1,142 +0,0 @@ -using back.DataModels; -using back.services.Crypto; -using Microsoft.EntityFrameworkCore; -using System.Net; - -namespace back.context; - -public class UserContext : DbContext -{ - public record HttpErrorMap(HttpStatusCode Code, string Description); - - public static class Errors - { - public static readonly HttpErrorMap Unauthorized = - new(HttpStatusCode.Unauthorized, "Invalid user data. Email or password are wrong."); - public static readonly HttpErrorMap BadRequest = - new(HttpStatusCode.BadRequest, "Missing user data."); - } - - public DbSet Users { get; set; } - private readonly ICryptoService _cryptoService; - public UserContext( - DbContextOptions options, - ICryptoService cryptoService - ) : base(options) - { - _cryptoService = cryptoService ?? throw new ArgumentNullException(nameof(cryptoService)); - // Ensure database is created - Database.EnsureCreated(); - } - - public async Task Create(string clientId, UserModel user) - { - ArgumentNullException.ThrowIfNull(user); - - if (await Exists(user)) - { - return await GetById(Guid.Parse(user.Id)) ?? null; - } - - if (string.IsNullOrEmpty(user.Id)) - { - user.Id = Guid.NewGuid().ToString(); - } - - if (string.IsNullOrEmpty(user.Salt)) - { - user.Salt = _cryptoService.Salt(); - } - user.Password = _cryptoService.Decrypt(clientId, user.Password) ?? string.Empty; - user.Password = _cryptoService.Hash(user.Password + user.Salt + _cryptoService.Pepper()) ?? string.Empty; - - user.CreatedAt = DateTime.UtcNow; - Users.Add(user); - await SaveChangesAsync(); - return user; - } - - public async Task Update(UserModel user) - { - ArgumentNullException.ThrowIfNull(user); - if (!await Exists(user)) - { - return null; - } - var existingUser = await GetById(Guid.Parse(user.Id)); - if (existingUser == null) return null; - existingUser.Name = user.Name; - existingUser.Email = user.Email; - existingUser.UpdatedAt = DateTime.UtcNow; - Users.Update(existingUser); - await SaveChangesAsync(); - return existingUser; - } - - public async Task Delete(Guid id) - { - var user = await GetById(id); - if (user == null) return false; - Users.Remove(user); - await SaveChangesAsync(); - return true; - } - - public async Task GetByEmail(string email) - { - if (string.IsNullOrEmpty(email)) return null; - return await Users.FirstOrDefaultAsync(u => u.Email == email); - } - - public async Task GetUserSaltByEmail(string email) - { - if (string.IsNullOrEmpty(email)) return string.Empty; - var user = await Users.FirstOrDefaultAsync(u => u.Email == email); - return user?.Salt ?? string.Empty; - } - - public async Task Login(string email, string password, string clientId) - { - if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null; - - var pass = _cryptoService.Decrypt(clientId, password) + await GetUserSaltByEmail(email) + _cryptoService.Pepper(); - var hashedPassword = _cryptoService.Hash(pass); - var user = await Users - .FirstOrDefaultAsync(u => u.Email == email && u.Password == hashedPassword); - return user; - } - - public async Task GetById(Guid id) - { - return await Users.FindAsync(id); - } - - public async Task GetTotalItems() - { - return await Users.CountAsync(); - } - - public async Task> GetPage(int page = 1, int pageSize = 20) - { - if (page < 1) page = 1; - if (pageSize < 1) pageSize = 20; - - return await Users - .OrderByDescending(p => p.CreatedAt) - .Skip((page - 1) * pageSize) - .Take(pageSize) - .ToListAsync(); - } - - public async Task Exists(UserModel? photo) - { - if (photo == null) return false; - if (string.IsNullOrEmpty(photo.Id)) return false; - return await Users.AnyAsync(p => p.Id == photo.Id); - } - - public async Task Exists(string id) - { - return await Users.AnyAsync(p => p.Id == id); - } -} \ No newline at end of file diff --git a/back/controllers/CryptoController.cs b/back/controllers/CryptoController.cs new file mode 100644 index 0000000..90da9c6 --- /dev/null +++ b/back/controllers/CryptoController.cs @@ -0,0 +1,21 @@ +using back.services.engine.Crypto; +using Microsoft.AspNetCore.Mvc; +using System.Net; + +namespace back.controllers; + +[ApiController, Route("api/[controller]")] +public class CryptoController(ICryptoService cryptoService) : ControllerBase +{ + [HttpGet("[action]")] public async Task RSA([FromHeader(Name = "X-client-thumbprint")] string clientId) + { + if (string.IsNullOrWhiteSpace(clientId)) + { + return BadRequest("Client ID is required."); + } + var key = cryptoService.GetPublicCertificate(clientId); + if (key == null) + return StatusCode((int)HttpStatusCode.InternalServerError, "Failed to generate RSA keys."); + return Ok(new { PublicKey = key }); + } +} diff --git a/back/controllers/PhotosController.cs b/back/controllers/PhotosController.cs index 1916a83..3be5755 100644 --- a/back/controllers/PhotosController.cs +++ b/back/controllers/PhotosController.cs @@ -1,60 +1,33 @@ -using back.context; -using back.DataModels; +using back.DataModels; using back.DTO; -using back.persistance.blob; +using back.services.bussines.PhotoService; using Microsoft.AspNetCore.Mvc; namespace back.controllers; [Route("api/[controller]")] [ApiController] -public class PhotosController(PhotoContext photoContext, IBlobStorageService blobStorage) : ControllerBase +public class PhotosController(IPhotoService photoService) : ControllerBase { - private readonly PhotoContext _photoContext = photoContext; + private readonly IPhotoService _photoService = photoService; // GET: api/ [HttpGet] - public async Task>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20) + public async Task>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20) { - var photos = await _photoContext.GetPage(page, pageSize); - var totalItems = await _photoContext.GetTotalItems(); - + (int totalItems, IEnumerable? pageData) = await _photoService.GetPage(page, pageSize); Response.Headers.Append("X-Total-Count", totalItems.ToString()); - return Ok(photos); + return Ok(pageData); } // GET api//5 - [HttpGet("{res}/{id}")] - public async Task Get(string res, Guid id) + [HttpGet("{id}/{res}")] + public async Task Get(string id, string res) { - var photo = await _photoContext.GetById(id); - if (photo == null) + (string? mediaType, byte[]? fileBytes) = await _photoService.GetBytes(id, res.ToLower()); + if(fileBytes == null) return NotFound(); - - string? filePath = res.ToLower() switch - { - "high" => photo.HighResUrl, - "mid" => photo.MidResUrl, - "low" or _ => photo.LowResUrl - }; - - string? mediaType = res.ToLower() switch - { - "high" => $"image/{photo.Extension}", - "mid" or "low" or _ => "image/webp", - }; - - if (filePath == null) - { - return NotFound(); - } - var file = await blobStorage.GetBytesAsync(filePath); - if (file == null) - { - return NotFound(); - } - - return File(file, mediaType); + return File(fileBytes, mediaType ?? "image/jpeg"); } // POST api/ @@ -66,7 +39,7 @@ public class PhotosController(PhotoContext photoContext, IBlobStorageService blo if (form.Image == null || form.Image.Length == 0) return BadRequest("No image uploaded."); - await _photoContext.CreateNew(form); + await _photoService.Create(form); return Created(); } @@ -78,21 +51,17 @@ public class PhotosController(PhotoContext photoContext, IBlobStorageService blo //// PUT api/ [HttpPut] - public async Task Put([FromBody] PhotoModel photo) + public async Task Put([FromBody] Photo photo) { - await _photoContext.Update(photo); + await _photoService.Update(photo); return NoContent(); } // DELETE api//5 [HttpDelete("{id}")] - public async Task Delete(Guid id) + public async Task Delete(string id) { - var photo = await _photoContext.GetById(id); - if (photo == null) - return NotFound(); - - await _photoContext.Delete(photo); + await _photoService.Delete(id); return NoContent(); } } diff --git a/back/controllers/UsersController.cs b/back/controllers/UsersController.cs index 83a1e7c..08d2b28 100644 --- a/back/controllers/UsersController.cs +++ b/back/controllers/UsersController.cs @@ -1,14 +1,18 @@ -using back.context; -using back.DataModels; +using back.DataModels; +using back.services.bussines; +using back.services.bussines.UserService; using Microsoft.AspNetCore.Mvc; -using System.Net; namespace back.controllers; +public record UserLoginFromModel(string Email, string Password, string? SystemKey); +public record ForgotPasswordFromModel(string Email); +public record RegisterFromModel(string Name, string Email, string Password); + [ApiController, Route("api/[controller]")] -public class UsersController(UserContext userContext) : ControllerBase +public class UsersController(IUserService user) : ControllerBase { - private readonly UserContext _userContext = userContext; + private readonly IUserService _user = user; // GET: api/ //[HttpGet] //public async Task>> Get([FromQuery] int page = 1, [FromQuery] int pageSize = 20) @@ -28,27 +32,59 @@ public class UsersController(UserContext userContext) : ControllerBase // return Ok(user); //} - [HttpPost] + [HttpPost("[action]")] public async Task Login( [FromHeader(Name = "X-client-thumbprint")] string clientId, - [FromBody] UserModel user + [FromBody] UserLoginFromModel user ) { + if (string.IsNullOrEmpty(clientId)) + return BadRequest("Client ID cannot be null or empty"); + if (user == null || string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password)) - return BadRequest(UserContext.Errors.BadRequest.Description); - var existingUser = await _userContext.Login(user.Email, user.Password, clientId); + return BadRequest(Errors.BadRequest.Description); + + if (user.Email.Equals("@system", StringComparison.InvariantCultureIgnoreCase)) + { + if (string.IsNullOrEmpty(user.SystemKey)) + return Unauthorized(Errors.Unauthorized.Description); + var systemUser = await _user.ValidateSystemUser(user.Email, user.Password, user.SystemKey, clientId); + if (systemUser == null) + return Unauthorized(Errors.Unauthorized.Description); + return Ok(systemUser); + } + + var existingUser = await _user.Login(user.Email, user.Password, clientId); if (existingUser == null) - return Unauthorized(UserContext.Errors.Unauthorized.Description); - return Ok(existingUser.ToDto()); + return Unauthorized(Errors.Unauthorized.Description); + return Ok(existingUser); } - //// POST api/ - //[HttpPost] - //public async Task Post([FromBody] UserModel user) - //{ - // if (user == null) - // return BadRequest("User cannot be null"); - // var createdUser = await _userContext.Create(user); - // return CreatedAtAction(nameof(Get), new { id = createdUser.Id }, createdUser); - //} + [HttpPost("forgot-password")] + public async Task ForgotPassword([FromBody] ForgotPasswordFromModel user) + { + if (string.IsNullOrEmpty(user.Email)) + return BadRequest("Email cannot be null or empty"); + await _user.SendResetPassword(user.Email); + return Ok("If the email exists, a reset password link has been sent."); + } + + // POST api/ + [HttpPost("[action]")] + public async Task Register( + [FromHeader(Name = "X-client-thumbprint")] string clientId, + [FromBody] RegisterFromModel user) + { + if (user == null) + return BadRequest("User cannot be null"); + try + { + var createdUser = await _user.Create(clientId, new User() { Email = user.Email, Password = user.Password }); + return Created(); + } + catch (Exception ex) + { + return BadRequest(ex); + } + } } diff --git a/back/persistance/blob/FileSystemImageStorageService.cs b/back/persistance/blob/FileSystemImageStorageService.cs index d08f174..1853211 100644 --- a/back/persistance/blob/FileSystemImageStorageService.cs +++ b/back/persistance/blob/FileSystemImageStorageService.cs @@ -5,41 +5,41 @@ using Microsoft.Extensions.Options; namespace back.persistance.blob; public class FileSystemImageStorageService( - IOptions systemOptions, + IOptions systemOptions, IOptionsMonitor options, IMemoryCache memoryCache - ) : IBlobStorageService + ) : IBlobStorageService { - private readonly string RootPath = systemOptions.Value.BaseDirectory ?? ""; + private readonly string RootPath = systemOptions.Value.BaseDirectory ?? "data"; private readonly DatabaseConfig config = options.Get(DatabaseConfig.BlobStorage); private readonly IMemoryCache cache = memoryCache; private string GetFullPath(string fileName) { // Ensure the directory exists - var directory = Path.Join(RootPath, config.SystemContainer, fileName); + var path = Path.Join(RootPath, config.SystemContainer, fileName); + var directory = Path.GetDirectoryName(path); if (directory != null && !Directory.Exists(directory)) { Directory.CreateDirectory(directory); } - return fileName; + return path; } - public async Task DeleteAsync(string fileName) + public async Task Delete(string fileName) { - var path = GetFullPath(fileName); - if (cache.TryGetValue(path, out Stream cachedStream)) - { - cachedStream.Dispose(); // Dispose the cached stream if it exists - cache.Remove(path); // Remove from cache - } - if (!File.Exists(path)) - { - return; // No file to delete - } try { - File.Delete(path); + var path = GetFullPath(fileName); + if (cache.TryGetValue(path, out Stream? cachedStream)) + { + cachedStream?.Dispose(); // Dispose the cached stream if it exists + cache.Remove(path); // Remove from cache + } + if (File.Exists(path)) + { + File.Delete(path); + } } catch (Exception ex) { @@ -47,14 +47,14 @@ public class FileSystemImageStorageService( } } - public Task GetStreamAsync(string fileName) + public async Task GetStream(string fileName) { var path = GetFullPath(fileName); if (File.Exists(path)) { if (cache.TryGetValue(path, out Stream? cachedStream)) { - return Task.FromResult(cachedStream); + return cachedStream; } // open the file stream for multiple reads and cache it for performance var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read); @@ -63,14 +63,14 @@ public class FileSystemImageStorageService( .SetValue(fileStream) .SetSlidingExpiration(TimeSpan.FromMinutes(30)); // Cache for 30 minutes - return Task.FromResult(fileStream); + return fileStream; } - return Task.FromResult(null); + return null; } - public async Task GetBytesAsync(string fileName) + public async Task GetBytes(string fileName) { - var stream = await GetStreamAsync(fileName); + var stream = await GetStream(fileName); if (stream != null) { using var memoryStream = new MemoryStream(); @@ -80,25 +80,25 @@ public class FileSystemImageStorageService( return null; } - public async Task SaveAsync(Stream blobStream, string fileName) + public async Task Save(Stream blobStream, string fileName) { var path = GetFullPath(fileName); if (cache.TryGetValue(path, out Stream? _) || File.Exists(path)) { throw new InvalidOperationException($"File {fileName} already exists. Use Update for updating file info."); } - using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write); + using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read); await blobStream.CopyToAsync(fileStream); } - public async Task UpdateAsync(Stream blobStream, string fileName) + public async Task Update(Stream blobStream, string fileName) { var path = GetFullPath(fileName); if (File.Exists(path)) { - await DeleteAsync(fileName); + await Delete(fileName); } - await SaveAsync(blobStream, fileName); + await Save(blobStream, fileName); } } diff --git a/back/persistance/blob/IBlobStorageService.cs b/back/persistance/blob/IBlobStorageService.cs index d276416..1ca5f14 100644 --- a/back/persistance/blob/IBlobStorageService.cs +++ b/back/persistance/blob/IBlobStorageService.cs @@ -1,11 +1,13 @@ -namespace back.persistance.blob; +using DependencyInjector.Lifetimes; -public interface IBlobStorageService +namespace back.persistance.blob; + +public interface IBlobStorageService : ISingleton { - Task SaveAsync(Stream blobStream, string fileName); - Task GetStreamAsync(string fileName); - Task GetBytesAsync(string fileName); - Task DeleteAsync(string fileName); - Task UpdateAsync(Stream blobStream, string fileName); + Task Save(Stream blobStream, string fileName); + Task GetStream(string fileName); + Task GetBytes(string fileName); + Task Delete(string fileName); + Task Update(Stream blobStream, string fileName); } diff --git a/back/persistance/data/DataContext.cs b/back/persistance/data/DataContext.cs new file mode 100644 index 0000000..5e32618 --- /dev/null +++ b/back/persistance/data/DataContext.cs @@ -0,0 +1,65 @@ +using back.DataModels; +using back.persistance.data.relations; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data; + +public partial class DataContext : DbContext +{ + public DataContext() { } + public DataContext(DbContextOptions options) : base(options) { } + + public virtual DbSet EfmigrationsLocks { get; set; } + + public virtual DbSet Events { get; set; } + + public virtual DbSet Galleries { get; set; } + + public virtual DbSet Permissions { get; set; } + + public virtual DbSet Persons { get; set; } + + public virtual DbSet Photos { get; set; } + + public virtual DbSet Rankings { get; set; } + + public virtual DbSet Roles { get; set; } + + public virtual DbSet SocialMedia { get; set; } + + public virtual DbSet Tags { get; set; } + + public virtual DbSet Users { get; set; } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.ToTable("__EFMigrationsLock"); + + entity.Property(e => e.Id).ValueGeneratedNever(); + }); + + typeof(IRelationEstablisher).Assembly.GetExportedTypes() + .Where(t => typeof(IRelationEstablisher).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract) + .ToList() + .ForEach(seederType => + { + var relationEstablisher = (IRelationEstablisher?)Activator.CreateInstance(seederType); + relationEstablisher?.EstablishRelation(modelBuilder); + }); + + //typeof(ISeeder).Assembly.GetExportedTypes() + // .Where(t => typeof(ISeeder).IsAssignableFrom(t) && !t.IsInterface && !t.IsAbstract) + // .ToList() + // .ForEach(seederType => + // { + // var seeder = (ISeeder?)Activator.CreateInstance(seederType); + // seeder?.Seed(modelBuilder); + // }); + + OnModelCreatingPartial(modelBuilder); + } + + partial void OnModelCreatingPartial(ModelBuilder modelBuilder); +} diff --git a/back/persistance/data/migrations/20250824120656_InitialSetup.Designer.cs b/back/persistance/data/migrations/20250824120656_InitialSetup.Designer.cs new file mode 100644 index 0000000..6b5d8ce --- /dev/null +++ b/back/persistance/data/migrations/20250824120656_InitialSetup.Designer.cs @@ -0,0 +1,752 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using back.persistance.data; + +#nullable disable + +namespace back.Migrations +{ + [DbContext(typeof(DataContext))] + [Migration("20250824120656_InitialSetup")] + partial class InitialSetup + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventTag", b => + { + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("EventId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("EventTags", (string)null); + }); + + modelBuilder.Entity("GalleryPhoto", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "PhotoId"); + + b.HasIndex("PhotoId"); + + b.ToTable("GalleryPhotos", (string)null); + }); + + modelBuilder.Entity("GalleryTag", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("GalleryTags", (string)null); + }); + + modelBuilder.Entity("GalleryUserViewer", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("GalleryUserViewers", (string)null); + }); + + modelBuilder.Entity("PhotoPerson", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "PersonId"); + + b.HasIndex("PersonId"); + + b.ToTable("PhotoPersons", (string)null); + }); + + modelBuilder.Entity("PhotoTag", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("PhotoTags", (string)null); + }); + + modelBuilder.Entity("PhotoUserBuyer", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("PhotoUserBuyers", (string)null); + }); + + modelBuilder.Entity("RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("PermissionId") + .HasColumnType("TEXT"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("RolePermissions", (string)null); + }); + + modelBuilder.Entity("UserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("back.DataModels.EfmigrationsLock", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("__EFMigrationsLock", (string)null); + }); + + modelBuilder.Entity("back.DataModels.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("back.DataModels.Gallery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("Title") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EventId"); + + b.ToTable("Galleries"); + }); + + modelBuilder.Entity("back.DataModels.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProfilePicture") + .HasColumnType("TEXT"); + + b.Property("SocialMediaId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SocialMediaId"); + + b.ToTable("Persons"); + }); + + modelBuilder.Entity("back.DataModels.Photo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("HighResUrl") + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsFavorite") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("LowResUrl") + .HasColumnType("TEXT"); + + b.Property("MidResUrl") + .HasColumnType("TEXT"); + + b.Property("RankingId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EventId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("back.DataModels.Ranking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DownVotes") + .HasColumnType("INTEGER"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("UpVotes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Rankings"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BaseRoleModelId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoleModelId"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("back.DataModels.SocialMedia", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlueSky") + .HasColumnType("TEXT"); + + b.Property("Discord") + .HasColumnType("TEXT"); + + b.Property("Facebook") + .HasColumnType("TEXT"); + + b.Property("Instagram") + .HasColumnType("TEXT"); + + b.Property("Linkedin") + .HasColumnType("TEXT"); + + b.Property("Other") + .HasColumnType("TEXT"); + + b.Property("Pinterest") + .HasColumnType("TEXT"); + + b.Property("Reddit") + .HasColumnType("TEXT"); + + b.Property("Tiktok") + .HasColumnType("TEXT"); + + b.Property("Twitter") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SocialMedia"); + }); + + modelBuilder.Entity("back.DataModels.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "IX_Tags_Name") + .IsUnique(); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventTag", b => + { + b.HasOne("back.DataModels.Event", null) + .WithMany() + .HasForeignKey("EventId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryPhoto", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryTag", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryUserViewer", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoPerson", b => + { + b.HasOne("back.DataModels.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .IsRequired(); + + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoTag", b => + { + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoUserBuyer", b => + { + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("RolePermission", b => + { + b.HasOne("back.DataModels.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .IsRequired(); + + b.HasOne("back.DataModels.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .IsRequired(); + }); + + modelBuilder.Entity("UserRole", b => + { + b.HasOne("back.DataModels.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("back.DataModels.Gallery", b => + { + b.HasOne("back.DataModels.User", "CreatedByNavigation") + .WithMany("Galleries") + .HasForeignKey("CreatedBy") + .IsRequired(); + + b.HasOne("back.DataModels.Event", "Event") + .WithMany("Galleries") + .HasForeignKey("EventId"); + + b.Navigation("CreatedByNavigation"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.HasOne("back.DataModels.SocialMedia", "SocialMedia") + .WithMany("People") + .HasForeignKey("SocialMediaId"); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("back.DataModels.Photo", b => + { + b.HasOne("back.DataModels.Person", "CreatedByNavigation") + .WithMany("Photos") + .HasForeignKey("CreatedBy") + .IsRequired(); + + b.HasOne("back.DataModels.Event", "Event") + .WithMany("Photos") + .HasForeignKey("EventId"); + + b.Navigation("CreatedByNavigation"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.HasOne("back.DataModels.Role", "BaseRoleModel") + .WithMany("InverseBaseRoleModel") + .HasForeignKey("BaseRoleModelId"); + + b.Navigation("BaseRoleModel"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.HasOne("back.DataModels.Person", "IdNavigation") + .WithOne("User") + .HasForeignKey("back.DataModels.User", "Id") + .IsRequired(); + + b.Navigation("IdNavigation"); + }); + + modelBuilder.Entity("back.DataModels.Event", b => + { + b.Navigation("Galleries"); + + b.Navigation("Photos"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.Navigation("Photos"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.Navigation("InverseBaseRoleModel"); + }); + + modelBuilder.Entity("back.DataModels.SocialMedia", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.Navigation("Galleries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/back/persistance/data/migrations/20250824120656_InitialSetup.cs b/back/persistance/data/migrations/20250824120656_InitialSetup.cs new file mode 100644 index 0000000..2c9b59c --- /dev/null +++ b/back/persistance/data/migrations/20250824120656_InitialSetup.cs @@ -0,0 +1,583 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace back.Migrations +{ + /// + public partial class InitialSetup : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "__EFMigrationsLock", + columns: table => new + { + Id = table.Column(type: "INTEGER", nullable: false), + Timestamp = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK___EFMigrationsLock", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Events", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 50, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Date = table.Column(type: "TEXT", nullable: true), + Location = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: false), + CreatedBy = table.Column(type: "TEXT", nullable: true), + UpdatedBy = table.Column(type: "TEXT", nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Events", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Permissions", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 255, nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Permissions", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Rankings", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + TotalVotes = table.Column(type: "INTEGER", nullable: false), + UpVotes = table.Column(type: "INTEGER", nullable: false), + DownVotes = table.Column(type: "INTEGER", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Rankings", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Roles", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 250, nullable: true), + BaseRoleModelId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Roles", x => x.Id); + table.ForeignKey( + name: "FK_Roles_Roles_BaseRoleModelId", + column: x => x.BaseRoleModelId, + principalTable: "Roles", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "SocialMedia", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Facebook = table.Column(type: "TEXT", nullable: true), + Instagram = table.Column(type: "TEXT", nullable: true), + Twitter = table.Column(type: "TEXT", nullable: true), + BlueSky = table.Column(type: "TEXT", nullable: true), + Tiktok = table.Column(type: "TEXT", nullable: true), + Linkedin = table.Column(type: "TEXT", nullable: true), + Pinterest = table.Column(type: "TEXT", nullable: true), + Discord = table.Column(type: "TEXT", nullable: true), + Reddit = table.Column(type: "TEXT", nullable: true), + Other = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SocialMedia", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "Tags", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 25, nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Tags", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "RolePermissions", + columns: table => new + { + RoleId = table.Column(type: "TEXT", nullable: false), + PermissionId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RolePermissions", x => new { x.RoleId, x.PermissionId }); + table.ForeignKey( + name: "FK_RolePermissions_Permissions_PermissionId", + column: x => x.PermissionId, + principalTable: "Permissions", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_RolePermissions_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Persons", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Name = table.Column(type: "TEXT", maxLength: 100, nullable: false), + ProfilePicture = table.Column(type: "TEXT", nullable: true), + Avatar = table.Column(type: "TEXT", nullable: true), + SocialMediaId = table.Column(type: "TEXT", nullable: true), + Bio = table.Column(type: "TEXT", maxLength: 250, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: false), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Persons", x => x.Id); + table.ForeignKey( + name: "FK_Persons_SocialMedia_SocialMediaId", + column: x => x.SocialMediaId, + principalTable: "SocialMedia", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "EventTags", + columns: table => new + { + EventId = table.Column(type: "TEXT", nullable: false), + TagId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_EventTags", x => new { x.EventId, x.TagId }); + table.ForeignKey( + name: "FK_EventTags_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_EventTags_Tags_TagId", + column: x => x.TagId, + principalTable: "Tags", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Photos", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 100, nullable: false), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + Extension = table.Column(type: "TEXT", nullable: true), + LowResUrl = table.Column(type: "TEXT", nullable: true), + MidResUrl = table.Column(type: "TEXT", nullable: true), + HighResUrl = table.Column(type: "TEXT", nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: false), + UpdatedBy = table.Column(type: "TEXT", nullable: true), + EventId = table.Column(type: "TEXT", nullable: true), + RankingId = table.Column(type: "TEXT", nullable: true), + IsFavorite = table.Column(type: "INTEGER", nullable: true, defaultValue: 0), + IsPublic = table.Column(type: "INTEGER", nullable: true, defaultValue: 1), + IsArchived = table.Column(type: "INTEGER", nullable: true, defaultValue: 0) + }, + constraints: table => + { + table.PrimaryKey("PK_Photos", x => x.Id); + table.ForeignKey( + name: "FK_Photos_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Photos_Persons_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Persons", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Users", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Email = table.Column(type: "TEXT", nullable: false), + Password = table.Column(type: "TEXT", nullable: false), + Salt = table.Column(type: "TEXT", nullable: false), + CreatedAt = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Users", x => x.Id); + table.ForeignKey( + name: "FK_Users_Persons_Id", + column: x => x.Id, + principalTable: "Persons", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "PhotoPersons", + columns: table => new + { + PhotoId = table.Column(type: "TEXT", nullable: false), + PersonId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PhotoPersons", x => new { x.PhotoId, x.PersonId }); + table.ForeignKey( + name: "FK_PhotoPersons_Persons_PersonId", + column: x => x.PersonId, + principalTable: "Persons", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PhotoPersons_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "PhotoTags", + columns: table => new + { + PhotoId = table.Column(type: "TEXT", nullable: false), + TagId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PhotoTags", x => new { x.PhotoId, x.TagId }); + table.ForeignKey( + name: "FK_PhotoTags_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PhotoTags_Tags_TagId", + column: x => x.TagId, + principalTable: "Tags", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "Galleries", + columns: table => new + { + Id = table.Column(type: "TEXT", nullable: false), + Title = table.Column(type: "TEXT", maxLength: 100, nullable: true), + Description = table.Column(type: "TEXT", maxLength: 500, nullable: true), + CreatedAt = table.Column(type: "TEXT", nullable: true), + UpdatedAt = table.Column(type: "TEXT", nullable: true), + CreatedBy = table.Column(type: "TEXT", nullable: false), + IsPublic = table.Column(type: "INTEGER", nullable: true, defaultValue: 1), + IsArchived = table.Column(type: "INTEGER", nullable: true, defaultValue: 0), + IsFavorite = table.Column(type: "INTEGER", nullable: true, defaultValue: 0), + IsDeleted = table.Column(type: "INTEGER", nullable: false), + DeletedAt = table.Column(type: "TEXT", nullable: true), + EventId = table.Column(type: "TEXT", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_Galleries", x => x.Id); + table.ForeignKey( + name: "FK_Galleries_Events_EventId", + column: x => x.EventId, + principalTable: "Events", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_Galleries_Users_CreatedBy", + column: x => x.CreatedBy, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "PhotoUserBuyers", + columns: table => new + { + PhotoId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_PhotoUserBuyers", x => new { x.PhotoId, x.UserId }); + table.ForeignKey( + name: "FK_PhotoUserBuyers_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_PhotoUserBuyers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "UserRoles", + columns: table => new + { + UserId = table.Column(type: "TEXT", nullable: false), + RoleId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_UserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_UserRoles_Roles_RoleId", + column: x => x.RoleId, + principalTable: "Roles", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_UserRoles_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "GalleryPhotos", + columns: table => new + { + GalleryId = table.Column(type: "TEXT", nullable: false), + PhotoId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GalleryPhotos", x => new { x.GalleryId, x.PhotoId }); + table.ForeignKey( + name: "FK_GalleryPhotos_Galleries_GalleryId", + column: x => x.GalleryId, + principalTable: "Galleries", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_GalleryPhotos_Photos_PhotoId", + column: x => x.PhotoId, + principalTable: "Photos", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "GalleryTags", + columns: table => new + { + GalleryId = table.Column(type: "TEXT", nullable: false), + TagId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GalleryTags", x => new { x.GalleryId, x.TagId }); + table.ForeignKey( + name: "FK_GalleryTags_Galleries_GalleryId", + column: x => x.GalleryId, + principalTable: "Galleries", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_GalleryTags_Tags_TagId", + column: x => x.TagId, + principalTable: "Tags", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "GalleryUserViewers", + columns: table => new + { + GalleryId = table.Column(type: "TEXT", nullable: false), + UserId = table.Column(type: "TEXT", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_GalleryUserViewers", x => new { x.GalleryId, x.UserId }); + table.ForeignKey( + name: "FK_GalleryUserViewers_Galleries_GalleryId", + column: x => x.GalleryId, + principalTable: "Galleries", + principalColumn: "Id"); + table.ForeignKey( + name: "FK_GalleryUserViewers_Users_UserId", + column: x => x.UserId, + principalTable: "Users", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_EventTags_TagId", + table: "EventTags", + column: "TagId"); + + migrationBuilder.CreateIndex( + name: "IX_Galleries_CreatedBy", + table: "Galleries", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Galleries_EventId", + table: "Galleries", + column: "EventId"); + + migrationBuilder.CreateIndex( + name: "IX_GalleryPhotos_PhotoId", + table: "GalleryPhotos", + column: "PhotoId"); + + migrationBuilder.CreateIndex( + name: "IX_GalleryTags_TagId", + table: "GalleryTags", + column: "TagId"); + + migrationBuilder.CreateIndex( + name: "IX_GalleryUserViewers_UserId", + table: "GalleryUserViewers", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_Persons_SocialMediaId", + table: "Persons", + column: "SocialMediaId"); + + migrationBuilder.CreateIndex( + name: "IX_PhotoPersons_PersonId", + table: "PhotoPersons", + column: "PersonId"); + + migrationBuilder.CreateIndex( + name: "IX_Photos_CreatedBy", + table: "Photos", + column: "CreatedBy"); + + migrationBuilder.CreateIndex( + name: "IX_Photos_EventId", + table: "Photos", + column: "EventId"); + + migrationBuilder.CreateIndex( + name: "IX_PhotoTags_TagId", + table: "PhotoTags", + column: "TagId"); + + migrationBuilder.CreateIndex( + name: "IX_PhotoUserBuyers_UserId", + table: "PhotoUserBuyers", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_RolePermissions_PermissionId", + table: "RolePermissions", + column: "PermissionId"); + + migrationBuilder.CreateIndex( + name: "IX_Roles_BaseRoleModelId", + table: "Roles", + column: "BaseRoleModelId"); + + migrationBuilder.CreateIndex( + name: "IX_Tags_Name", + table: "Tags", + column: "Name", + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_UserRoles_RoleId", + table: "UserRoles", + column: "RoleId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "__EFMigrationsLock"); + + migrationBuilder.DropTable( + name: "EventTags"); + + migrationBuilder.DropTable( + name: "GalleryPhotos"); + + migrationBuilder.DropTable( + name: "GalleryTags"); + + migrationBuilder.DropTable( + name: "GalleryUserViewers"); + + migrationBuilder.DropTable( + name: "PhotoPersons"); + + migrationBuilder.DropTable( + name: "PhotoTags"); + + migrationBuilder.DropTable( + name: "PhotoUserBuyers"); + + migrationBuilder.DropTable( + name: "Rankings"); + + migrationBuilder.DropTable( + name: "RolePermissions"); + + migrationBuilder.DropTable( + name: "UserRoles"); + + migrationBuilder.DropTable( + name: "Galleries"); + + migrationBuilder.DropTable( + name: "Tags"); + + migrationBuilder.DropTable( + name: "Photos"); + + migrationBuilder.DropTable( + name: "Permissions"); + + migrationBuilder.DropTable( + name: "Roles"); + + migrationBuilder.DropTable( + name: "Users"); + + migrationBuilder.DropTable( + name: "Events"); + + migrationBuilder.DropTable( + name: "Persons"); + + migrationBuilder.DropTable( + name: "SocialMedia"); + } + } +} diff --git a/back/persistance/data/migrations/DataContextModelSnapshot.cs b/back/persistance/data/migrations/DataContextModelSnapshot.cs new file mode 100644 index 0000000..69154c8 --- /dev/null +++ b/back/persistance/data/migrations/DataContextModelSnapshot.cs @@ -0,0 +1,749 @@ +// +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using back.persistance.data; + +#nullable disable + +namespace back.Migrations +{ + [DbContext(typeof(DataContext))] + partial class DataContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "9.0.8"); + + modelBuilder.Entity("EventTag", b => + { + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("EventId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("EventTags", (string)null); + }); + + modelBuilder.Entity("GalleryPhoto", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "PhotoId"); + + b.HasIndex("PhotoId"); + + b.ToTable("GalleryPhotos", (string)null); + }); + + modelBuilder.Entity("GalleryTag", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("GalleryTags", (string)null); + }); + + modelBuilder.Entity("GalleryUserViewer", b => + { + b.Property("GalleryId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("GalleryId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("GalleryUserViewers", (string)null); + }); + + modelBuilder.Entity("PhotoPerson", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("PersonId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "PersonId"); + + b.HasIndex("PersonId"); + + b.ToTable("PhotoPersons", (string)null); + }); + + modelBuilder.Entity("PhotoTag", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("TagId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "TagId"); + + b.HasIndex("TagId"); + + b.ToTable("PhotoTags", (string)null); + }); + + modelBuilder.Entity("PhotoUserBuyer", b => + { + b.Property("PhotoId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("TEXT"); + + b.HasKey("PhotoId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("PhotoUserBuyers", (string)null); + }); + + modelBuilder.Entity("RolePermission", b => + { + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.Property("PermissionId") + .HasColumnType("TEXT"); + + b.HasKey("RoleId", "PermissionId"); + + b.HasIndex("PermissionId"); + + b.ToTable("RolePermissions", (string)null); + }); + + modelBuilder.Entity("UserRole", b => + { + b.Property("UserId") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("TEXT"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("back.DataModels.EfmigrationsLock", b => + { + b.Property("Id") + .HasColumnType("INTEGER"); + + b.Property("Timestamp") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("__EFMigrationsLock", (string)null); + }); + + modelBuilder.Entity("back.DataModels.Event", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .HasColumnType("TEXT"); + + b.Property("Date") + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Location") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Events"); + }); + + modelBuilder.Entity("back.DataModels.Gallery", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("IsFavorite") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("Title") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EventId"); + + b.ToTable("Galleries"); + }); + + modelBuilder.Entity("back.DataModels.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(255) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("Avatar") + .HasColumnType("TEXT"); + + b.Property("Bio") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DeletedAt") + .HasColumnType("TEXT"); + + b.Property("IsDeleted") + .HasColumnType("INTEGER"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("ProfilePicture") + .HasColumnType("TEXT"); + + b.Property("SocialMediaId") + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("SocialMediaId"); + + b.ToTable("Persons"); + }); + + modelBuilder.Entity("back.DataModels.Photo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .HasColumnType("TEXT"); + + b.Property("CreatedBy") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("TEXT"); + + b.Property("EventId") + .HasColumnType("TEXT"); + + b.Property("Extension") + .HasColumnType("TEXT"); + + b.Property("HighResUrl") + .HasColumnType("TEXT"); + + b.Property("IsArchived") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsFavorite") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("IsPublic") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1); + + b.Property("LowResUrl") + .HasColumnType("TEXT"); + + b.Property("MidResUrl") + .HasColumnType("TEXT"); + + b.Property("RankingId") + .HasColumnType("TEXT"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("UpdatedAt") + .HasColumnType("TEXT"); + + b.Property("UpdatedBy") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("CreatedBy"); + + b.HasIndex("EventId"); + + b.ToTable("Photos"); + }); + + modelBuilder.Entity("back.DataModels.Ranking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("DownVotes") + .HasColumnType("INTEGER"); + + b.Property("TotalVotes") + .HasColumnType("INTEGER"); + + b.Property("UpVotes") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Rankings"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BaseRoleModelId") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasMaxLength(250) + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("BaseRoleModelId"); + + b.ToTable("Roles"); + }); + + modelBuilder.Entity("back.DataModels.SocialMedia", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("BlueSky") + .HasColumnType("TEXT"); + + b.Property("Discord") + .HasColumnType("TEXT"); + + b.Property("Facebook") + .HasColumnType("TEXT"); + + b.Property("Instagram") + .HasColumnType("TEXT"); + + b.Property("Linkedin") + .HasColumnType("TEXT"); + + b.Property("Other") + .HasColumnType("TEXT"); + + b.Property("Pinterest") + .HasColumnType("TEXT"); + + b.Property("Reddit") + .HasColumnType("TEXT"); + + b.Property("Tiktok") + .HasColumnType("TEXT"); + + b.Property("Twitter") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("SocialMedia"); + }); + + modelBuilder.Entity("back.DataModels.Tag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(25) + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex(new[] { "Name" }, "IX_Tags_Name") + .IsUnique(); + + b.ToTable("Tags"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT"); + + b.Property("CreatedAt") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Email") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Password") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Salt") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Users"); + }); + + modelBuilder.Entity("EventTag", b => + { + b.HasOne("back.DataModels.Event", null) + .WithMany() + .HasForeignKey("EventId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryPhoto", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryTag", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("GalleryUserViewer", b => + { + b.HasOne("back.DataModels.Gallery", null) + .WithMany() + .HasForeignKey("GalleryId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoPerson", b => + { + b.HasOne("back.DataModels.Person", null) + .WithMany() + .HasForeignKey("PersonId") + .IsRequired(); + + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoTag", b => + { + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + + b.HasOne("back.DataModels.Tag", null) + .WithMany() + .HasForeignKey("TagId") + .IsRequired(); + }); + + modelBuilder.Entity("PhotoUserBuyer", b => + { + b.HasOne("back.DataModels.Photo", null) + .WithMany() + .HasForeignKey("PhotoId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("RolePermission", b => + { + b.HasOne("back.DataModels.Permission", null) + .WithMany() + .HasForeignKey("PermissionId") + .IsRequired(); + + b.HasOne("back.DataModels.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .IsRequired(); + }); + + modelBuilder.Entity("UserRole", b => + { + b.HasOne("back.DataModels.Role", null) + .WithMany() + .HasForeignKey("RoleId") + .IsRequired(); + + b.HasOne("back.DataModels.User", null) + .WithMany() + .HasForeignKey("UserId") + .IsRequired(); + }); + + modelBuilder.Entity("back.DataModels.Gallery", b => + { + b.HasOne("back.DataModels.User", "CreatedByNavigation") + .WithMany("Galleries") + .HasForeignKey("CreatedBy") + .IsRequired(); + + b.HasOne("back.DataModels.Event", "Event") + .WithMany("Galleries") + .HasForeignKey("EventId"); + + b.Navigation("CreatedByNavigation"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.HasOne("back.DataModels.SocialMedia", "SocialMedia") + .WithMany("People") + .HasForeignKey("SocialMediaId"); + + b.Navigation("SocialMedia"); + }); + + modelBuilder.Entity("back.DataModels.Photo", b => + { + b.HasOne("back.DataModels.Person", "CreatedByNavigation") + .WithMany("Photos") + .HasForeignKey("CreatedBy") + .IsRequired(); + + b.HasOne("back.DataModels.Event", "Event") + .WithMany("Photos") + .HasForeignKey("EventId"); + + b.Navigation("CreatedByNavigation"); + + b.Navigation("Event"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.HasOne("back.DataModels.Role", "BaseRoleModel") + .WithMany("InverseBaseRoleModel") + .HasForeignKey("BaseRoleModelId"); + + b.Navigation("BaseRoleModel"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.HasOne("back.DataModels.Person", "IdNavigation") + .WithOne("User") + .HasForeignKey("back.DataModels.User", "Id") + .IsRequired(); + + b.Navigation("IdNavigation"); + }); + + modelBuilder.Entity("back.DataModels.Event", b => + { + b.Navigation("Galleries"); + + b.Navigation("Photos"); + }); + + modelBuilder.Entity("back.DataModels.Person", b => + { + b.Navigation("Photos"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("back.DataModels.Role", b => + { + b.Navigation("InverseBaseRoleModel"); + }); + + modelBuilder.Entity("back.DataModels.SocialMedia", b => + { + b.Navigation("People"); + }); + + modelBuilder.Entity("back.DataModels.User", b => + { + b.Navigation("Galleries"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/back/persistance/data/migrations/sqlite/tables.sql b/back/persistance/data/migrations/sqlite/tables.sql new file mode 100644 index 0000000..80af444 --- /dev/null +++ b/back/persistance/data/migrations/sqlite/tables.sql @@ -0,0 +1,200 @@ +-- Tabla de redes sociales (SocialMedia) y relación uno a uno con Person +CREATE TABLE IF NOT EXISTS SocialMedia ( + Id TEXT PRIMARY KEY, + Facebook TEXT, + Instagram TEXT, + Twitter TEXT, + BlueSky TEXT, + Tiktok TEXT, + Linkedin TEXT, + Pinterest TEXT, + Discord TEXT, + Reddit TEXT, + Other TEXT +); + +-- Person: cada persona tiene un grupo de redes sociales (uno a uno, fk opcional) +CREATE TABLE IF NOT EXISTS Persons ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL, + ProfilePicture TEXT, + Avatar TEXT, + SocialMediaId TEXT, + Bio TEXT, + CreatedAt TEXT NOT NULL, + UpdatedAt TEXT, + FOREIGN KEY (SocialMediaId) REFERENCES SocialMedia(Id) +); + +-- User: es una persona (herencia por clave primaria compartida) +CREATE TABLE IF NOT EXISTS Users ( + Id TEXT PRIMARY KEY, -- MISMA clave y valor que Persons.Id + Email TEXT NOT NULL, + Password TEXT NOT NULL, + Salt TEXT NOT NULL, + FOREIGN KEY (Id) REFERENCES Persons(Id) +); + +-- Un usuario puede ver muchas galerías (muchos-a-muchos: Galleries <-> Users) +CREATE TABLE IF NOT EXISTS GalleryUserViewers ( + GalleryId TEXT NOT NULL, + UserId TEXT NOT NULL, + PRIMARY KEY (GalleryId, UserId), + FOREIGN KEY (GalleryId) REFERENCES Galleries(Id), + FOREIGN KEY (UserId) REFERENCES Users(Id) +); + +-- Un usuario ha creado muchas galerías (uno a muchos) +-- Una galería solo puede ser creada por un usuario +CREATE TABLE IF NOT EXISTS Galleries ( + Id TEXT PRIMARY KEY, + Title TEXT, + Description TEXT, + CreatedAt TEXT, + UpdatedAt TEXT, + CreatedBy TEXT NOT NULL, -- FK a Users + IsPublic INTEGER DEFAULT 1, + IsArchived INTEGER DEFAULT 0, + IsFavorite INTEGER DEFAULT 0, + EventId TEXT, -- FK opcional a Events (una galería puede asociarse a un evento) + FOREIGN KEY (CreatedBy) REFERENCES Users(Id), + FOREIGN KEY (EventId) REFERENCES Events(Id) +); + +-- Galería-Photo: una galería contiene muchas imagenes, una imagen puede estar en muchas galerías (muchos-a-muchos) +CREATE TABLE IF NOT EXISTS GalleryPhotos ( + GalleryId TEXT NOT NULL, + PhotoId TEXT NOT NULL, + PRIMARY KEY (GalleryId, PhotoId), + FOREIGN KEY (GalleryId) REFERENCES Galleries(Id), + FOREIGN KEY (PhotoId) REFERENCES Photos(Id) +); + +-- Tabla de eventos +CREATE TABLE IF NOT EXISTS Events ( + Id TEXT PRIMARY KEY, + Title TEXT NOT NULL, + Description TEXT, + Date TEXT, + Location TEXT, + CreatedAt TEXT NOT NULL, + UpdatedAt TEXT NOT NULL, + CreatedBy TEXT, + UpdatedBy TEXT, + IsDeleted INTEGER NOT NULL DEFAULT 0, + DeletedAt TEXT +); + +-- Tabla de fotos +CREATE TABLE IF NOT EXISTS Photos ( + Id TEXT PRIMARY KEY, + Title TEXT NOT NULL, + Description TEXT, + Extension TEXT, + LowResUrl TEXT, + MidResUrl TEXT, + HighResUrl TEXT, + CreatedAt TEXT, + UpdatedAt TEXT, + CreatedBy TEXT NOT NULL, -- Persona que subió la foto: FK a Persons + UpdatedBy TEXT, + EventId TEXT, -- Una photo solo puede tener un evento asociado (FK) + RankingId TEXT, + IsFavorite INTEGER DEFAULT 0, + IsPublic INTEGER DEFAULT 1, + IsArchived INTEGER DEFAULT 0, + FOREIGN KEY (CreatedBy) REFERENCES Persons(Id), + FOREIGN KEY (EventId) REFERENCES Events(Id) +); + +-- Una persona puede salir en muchas fotos, y una foto puede tener muchas personas (muchos-a-muchos) +CREATE TABLE IF NOT EXISTS PhotoPersons ( + PhotoId TEXT NOT NULL, + PersonId TEXT NOT NULL, + PRIMARY KEY (PhotoId, PersonId), + FOREIGN KEY (PhotoId) REFERENCES Photos(Id), + FOREIGN KEY (PersonId) REFERENCES Persons(Id) +); + +-- Un usuario puede comprar muchas fotos para verlas, y una foto puede haber sido comprada por muchos usuarios +-- (solo necesario si IsPublic = 0) +CREATE TABLE IF NOT EXISTS PhotoUserBuyers ( + PhotoId TEXT NOT NULL, + UserId TEXT NOT NULL, + PRIMARY KEY (PhotoId, UserId), + FOREIGN KEY (PhotoId) REFERENCES Photos(Id), + FOREIGN KEY (UserId) REFERENCES Users(Id) +); + +-- Tabla de tags (únicos) +CREATE TABLE IF NOT EXISTS Tags ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL UNIQUE, + CreatedAt TEXT NOT NULL +); + +-- Una foto puede tener muchos tags (muchos-a-muchos) +CREATE TABLE IF NOT EXISTS PhotoTags ( + PhotoId TEXT NOT NULL, + TagId TEXT NOT NULL, + PRIMARY KEY (PhotoId, TagId), + FOREIGN KEY (PhotoId) REFERENCES Photos(Id), + FOREIGN KEY (TagId) REFERENCES Tags(Id) +); + +-- Un evento puede tener muchos tags (muchos-a-muchos) +CREATE TABLE IF NOT EXISTS EventTags ( + EventId TEXT NOT NULL, + TagId TEXT NOT NULL, + PRIMARY KEY (EventId, TagId), + FOREIGN KEY (EventId) REFERENCES Events(Id), + FOREIGN KEY (TagId) REFERENCES Tags(Id) +); + +-- Una galería puede tener muchos tags (muchos-a-muchos) +CREATE TABLE IF NOT EXISTS GalleryTags ( + GalleryId TEXT NOT NULL, + TagId TEXT NOT NULL, + PRIMARY KEY (GalleryId, TagId), + FOREIGN KEY (GalleryId) REFERENCES Galleries(Id), + FOREIGN KEY (TagId) REFERENCES Tags(Id) +); + +-- Rankings (por si corresponde) +CREATE TABLE IF NOT EXISTS Rankings ( + Id TEXT PRIMARY KEY, + TotalVotes INTEGER NOT NULL, + UpVotes INTEGER NOT NULL, + DownVotes INTEGER NOT NULL +); + +-- Permissions y Roles, tal y como en el mensaje anterior... +CREATE TABLE IF NOT EXISTS Permissions ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL, + Description TEXT +); + +CREATE TABLE IF NOT EXISTS Roles ( + Id TEXT PRIMARY KEY, + Name TEXT NOT NULL, + Description TEXT, + BaseRoleModelId TEXT, + FOREIGN KEY (BaseRoleModelId) REFERENCES Roles(Id) +); + +CREATE TABLE IF NOT EXISTS UserRoles ( + UserId TEXT NOT NULL, + RoleId TEXT NOT NULL, + PRIMARY KEY (UserId, RoleId), + FOREIGN KEY (UserId) REFERENCES Users(Id), + FOREIGN KEY (RoleId) REFERENCES Roles(Id) +); + +CREATE TABLE IF NOT EXISTS RolePermissions ( + RoleId TEXT NOT NULL, + PermissionId TEXT NOT NULL, + PRIMARY KEY (RoleId, PermissionId), + FOREIGN KEY (RoleId) REFERENCES Roles(Id), + FOREIGN KEY (PermissionId) REFERENCES Permissions(Id) +); diff --git a/back/persistance/data/relations/EventRelationEstablisher.cs b/back/persistance/data/relations/EventRelationEstablisher.cs new file mode 100644 index 0000000..25241da --- /dev/null +++ b/back/persistance/data/relations/EventRelationEstablisher.cs @@ -0,0 +1,28 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class EventRelationEstablisher: IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasMany(d => d.Tags).WithMany(p => p.Events) + .UsingEntity>( + "EventTag", + r => r.HasOne().WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("EventId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("EventId", "TagId"); + j.ToTable("EventTags"); + }); + }); + } +} diff --git a/back/persistance/data/relations/GalleryRelationEstablisher.cs b/back/persistance/data/relations/GalleryRelationEstablisher.cs new file mode 100644 index 0000000..b1a0e7f --- /dev/null +++ b/back/persistance/data/relations/GalleryRelationEstablisher.cs @@ -0,0 +1,68 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class GalleryRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.Property(e => e.IsArchived).HasDefaultValue(0); + entity.Property(e => e.IsFavorite).HasDefaultValue(0); + entity.Property(e => e.IsPublic).HasDefaultValue(1); + + entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Galleries) + .HasForeignKey(d => d.CreatedBy) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.Event).WithMany(p => p.Galleries).HasForeignKey(d => d.EventId); + + entity.HasMany(d => d.Photos).WithMany(p => p.Galleries) + .UsingEntity>( + "GalleryPhoto", + r => r.HasOne().WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("GalleryId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("GalleryId", "PhotoId"); + j.ToTable("GalleryPhotos"); + }); + + entity.HasMany(d => d.Tags).WithMany(p => p.Galleries) + .UsingEntity>( + "GalleryTag", + r => r.HasOne().WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("GalleryId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("GalleryId", "TagId"); + j.ToTable("GalleryTags"); + }); + + entity.HasMany(d => d.Users).WithMany(p => p.GalleriesNavigation) + .UsingEntity>( + "GalleryUserViewer", + r => r.HasOne().WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("GalleryId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("GalleryId", "UserId"); + j.ToTable("GalleryUserViewers"); + }); + }); + } +} \ No newline at end of file diff --git a/back/persistance/data/relations/IRelationEstablisher.cs b/back/persistance/data/relations/IRelationEstablisher.cs new file mode 100644 index 0000000..5d2e4ae --- /dev/null +++ b/back/persistance/data/relations/IRelationEstablisher.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public interface IRelationEstablisher +{ + void EstablishRelation(ModelBuilder modelBuilder); +} diff --git a/back/persistance/data/relations/PersonRelationEstablisher.cs b/back/persistance/data/relations/PersonRelationEstablisher.cs new file mode 100644 index 0000000..245ed4a --- /dev/null +++ b/back/persistance/data/relations/PersonRelationEstablisher.cs @@ -0,0 +1,68 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class PersonRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.Property(e => e.IsArchived).HasDefaultValue(0); + entity.Property(e => e.IsFavorite).HasDefaultValue(0); + entity.Property(e => e.IsPublic).HasDefaultValue(1); + + entity.HasOne(d => d.CreatedByNavigation).WithMany(p => p.Photos) + .HasForeignKey(d => d.CreatedBy) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasOne(d => d.Event).WithMany(p => p.Photos).HasForeignKey(d => d.EventId); + + entity.HasMany(d => d.People).WithMany(p => p.PhotosNavigation) + .UsingEntity>( + "PhotoPerson", + r => r.HasOne().WithMany() + .HasForeignKey("PersonId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("PhotoId", "PersonId"); + j.ToTable("PhotoPersons"); + }); + + entity.HasMany(d => d.Tags).WithMany(p => p.Photos) + .UsingEntity>( + "PhotoTag", + r => r.HasOne().WithMany() + .HasForeignKey("TagId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("PhotoId", "TagId"); + j.ToTable("PhotoTags"); + }); + + entity.HasMany(d => d.Users).WithMany(p => p.Photos) + .UsingEntity>( + "PhotoUserBuyer", + r => r.HasOne().WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("PhotoId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("PhotoId", "UserId"); + j.ToTable("PhotoUserBuyers"); + }); + }); + } +} diff --git a/back/persistance/data/relations/PhotoContext.cs b/back/persistance/data/relations/PhotoContext.cs new file mode 100644 index 0000000..e931ea7 --- /dev/null +++ b/back/persistance/data/relations/PhotoContext.cs @@ -0,0 +1,307 @@ +//using back.DataModels; +//using back.DTO; +//using back.persistance.blob; +//using back.services.ImageResizer; +//using Microsoft.EntityFrameworkCore; +//using Microsoft.Extensions.Hosting.Internal; + +//namespace back.persistance.data.relations; + +//public class PhotoContext : DbContext +//{ + +// private readonly IImageResizer _Resizer; +// private readonly IBlobStorageService _BlobStorage; + +// private readonly TagContext _tagContext; +// private readonly EventContext _eventContext; +// private readonly PersonRelationEstablisher _personContext; + +// public PhotoContext(DbContextOptions options, IHostEnvironment hostingEnvironment, +// IImageResizer resizer, +// IBlobStorageService blobStorage, +// TagContext tags, +// EventContext events, +// PersonRelationEstablisher persons +// ) : base(options) +// { +// _Resizer = resizer; +// _BlobStorage = blobStorage; +// _tagContext = tags; +// _eventContext = events; +// _personContext = persons; + +// if (hostingEnvironment.IsDevelopment()) +// { +// Database.EnsureCreated(); +// } +// else +// { +// Database.Migrate(); +// } +// } + +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// // Photo -> Tags (muchos-a-muchos) +// modelBuilder.Entity() +// .HasMany(p => p.Tags) +// .WithMany(t => t.Photos) +// .UsingEntity(j => j.ToTable("PhotoTags")); + +// // Photo -> Persons (muchos-a-muchos) +// modelBuilder.Entity() +// .HasMany(p => p.PersonsIn) +// .WithMany(per => per.Photos) +// .UsingEntity(j => j.ToTable("PhotoPersons")); + +// // Photo -> Event (muchos-a-uno) +// modelBuilder.Entity() +// .HasOne(p => p.Event) +// .WithMany() // Un evento puede tener múltiples fotos +// .HasForeignKey(p => p.EventId); + +// // Photo -> Ranking (uno-a-uno) +// modelBuilder.Entity() +// .HasOne(p => p.Ranking) +// .WithOne(r => r.Photo) // Un ranking está asociado a una sola foto +// .HasForeignKey(p => p.RankingId); + +// base.OnModelCreating(modelBuilder); +// } + +// public async Task CreateNew(PhotoFormModel? form) +// { +// if (form == null) { return; } + +// var photo = new Photo( +// Guid.NewGuid().ToString(), +// form.Title, +// form.Description ?? string.Empty, +// string.Empty, // LowResUrl will be set later +// string.Empty, // MidResUrl will be set later +// string.Empty, // HighResUrl will be set later +// DateTime.UtcNow, +// DateTime.UtcNow, +// form.UserId, +// form.UserId +// ) +// { +// IsPublic = form.IsPublic +// }; + +// List tasks = [ +// SaveBlob(photo, form), +// LinkTags(photo, form.Tags ?? [], form.UserId), +// LinkEvent(photo, form.Evento ?? "", form.UserId), +// LinkPersons(photo, form.People ?? [], form.UserId), +// ]; + +// await Task.WhenAll(tasks); +// await Photos.AddAsync(photo); +// await SaveChangesAsync(); +// } + +// private async Task LinkPersons(Photo photo, string[] personas, string updatedBy = "SYSTEM") +// { +// if (photo == null || personas == null || personas.Length == 0) return; +// foreach (var personId in personas) +// { +// var person = await _personContext.GetById(personId); +// if (person != null) +// { +// await LinkPersons(photo, person, updatedBy); +// } +// } +// } + +// private async Task LinkPersons(Photo photo, Person tag, string updatedBy = "SYSTEM") +// { +// if (tag == null) return; +// // Ensure the tag exists +// if (await _personContext.Exists(tag.Id)) +// { +// photo.PersonsIn ??= []; +// photo.PersonsIn.Add(tag); +// photo.UpdatedAt = DateTime.UtcNow; +// photo.UpdatedBy = updatedBy; // or use a more appropriate value +// } +// } + +// private async Task LinkTags(Photo photo, string[] tags, string updatedBy = "SYSTEM") +// { +// if (photo == null || tags == null || tags.Length == 0) return; +// foreach (var tagId in tags) +// { +// var tag = await _tagContext.GetById(tagId); +// if (tag != null) +// { +// await LinkTag(photo, tag, updatedBy); +// } +// } +// } + +// private async Task LinkTag(Photo photo, Tag tag, string updatedBy = "SYSTEM") +// { +// if (tag == null) return; +// // Ensure the tag exists +// if (await _tagContext.Exists(tag.Id)) +// { +// photo.Tags.Add(tag); +// photo.UpdatedAt = DateTime.UtcNow; +// photo.UpdatedBy = updatedBy; // or use a more appropriate value +// } +// } + +// private async Task LinkEvent(Photo photo, string eventId, string updatedBy = "SYSTEM") +// { +// if (string.IsNullOrEmpty(eventId)) return; +// var evento = await _eventContext.GetById(eventId); +// if (evento != null) +// { +// await LinkEvent(photo, evento, updatedBy); +// } +// } + +// private async Task LinkEvent(Photo photo, Event? evento, string updatedBy = "SYSTEM") +// { +// if (evento == null) return; +// // Ensure the event exists +// if (await _eventContext.Exists(evento.Id)) +// { +// photo.Event = evento; +// photo.UpdatedAt = DateTime.UtcNow; +// photo.UpdatedBy = updatedBy; +// } +// } + +// private async Task SaveBlob(Photo photo, PhotoFormModel form) +// { +// if (form.Image != null && form.Image.Length > 0) +// { +// var lowRes = await _Resizer.ResizeImage(form.Image, 480); +// var midRes = await _Resizer.ResizeImage(form.Image, 720); +// // Upload images to blob storage +// photo.Extension = form.Image.FileName.Split('.').Last(); +// photo.LowResUrl = $"low/{photo.Id}.webp"; +// photo.MidResUrl = $"mid/{photo.Id}.webp"; +// photo.HighResUrl = $"high/{photo.Id}.{photo.Extension}"; +// await _BlobStorage.SaveAsync(lowRes, photo.LowResUrl); +// await _BlobStorage.SaveAsync(midRes, photo.MidResUrl); +// await _BlobStorage.SaveAsync(form.Image.OpenReadStream(), photo.HighResUrl); +// } +// } + +// public async Task GetById(string id) +// { +// return await GetById(Guid.Parse(id)); +// } + +// public async Task GetById(Guid id) +// { +// try +// { +// return await Photos.FindAsync(id); +// } +// catch +// { +// return null; +// } +// } + +// public async Task GetTotalItems() +// { +// try +// { +// return await Photos.CountAsync(); +// } +// catch +// { +// return 0; +// } +// } + +// public async Task?> GetPage(int page = 1, int pageSize = 20) +// { +// if (page < 1) page = 1; +// if (pageSize < 1) pageSize = 20; +// try +// { +// return await Photos +// .OrderByDescending(p => p.CreatedAt) +// .Skip((page - 1) * pageSize) +// .Take(pageSize) +// .ToListAsync(); +// } +// catch +// { +// return null; +// } +// } + +// public async Task Exists(Photo? photo) +// { +// try +// { +// if (photo == null) return false; +// if (string.IsNullOrEmpty(photo.Id)) return false; +// return await Photos.AnyAsync(p => p.Id == photo.Id); +// } +// catch +// { +// return false; // Handle exceptions gracefully +// } +// } + +// public async Task Exists(string id) +// { +// try +// { +// if (string.IsNullOrEmpty(id)) return false; +// return await Photos.AnyAsync(p => p.Id == id); +// } +// catch +// { +// return false; // Handle exceptions gracefully +// } +// } + +// public async Task Delete(Photo photo) +// { +// if (photo == null) return; +// if (await Exists(photo)) +// { +// // Delete the photo from blob storage +// if (!string.IsNullOrEmpty(photo.LowResUrl)) +// await _BlobStorage.DeleteAsync(photo.LowResUrl); +// if (!string.IsNullOrEmpty(photo.MidResUrl)) +// await _BlobStorage.DeleteAsync(photo.MidResUrl); +// if (!string.IsNullOrEmpty(photo.HighResUrl)) +// await _BlobStorage.DeleteAsync(photo.HighResUrl); +// Photos.Remove(photo); +// await SaveChangesAsync(); +// } +// } + +// public async Task Update(Photo photo) +// { +// if (photo == null) return; +// if (await Exists(photo)) +// { +// var evento = photo.Event; +// photo.Event = null; +// await LinkEvent(photo, evento, photo.UpdatedBy); + +// var tags = photo.Tags.Select(t => t.Id); +// photo.Tags.Clear(); +// await LinkTags(photo, [.. tags], photo.UpdatedBy); + +// var persons = photo.PersonsIn?.Select(t => t.Id) ?? []; +// photo.PersonsIn = null; +// await LinkPersons(photo, [.. persons], photo.UpdatedBy); + +// Photos.Update(photo); +// await SaveChangesAsync(); +// } +// } +//} diff --git a/back/persistance/data/relations/PhotoRelationEstablisher.cs b/back/persistance/data/relations/PhotoRelationEstablisher.cs new file mode 100644 index 0000000..ec6fbf7 --- /dev/null +++ b/back/persistance/data/relations/PhotoRelationEstablisher.cs @@ -0,0 +1,15 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class PhotoRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.SocialMedia).WithMany(p => p.People).HasForeignKey(d => d.SocialMediaId); + }); + } +} diff --git a/back/persistance/data/relations/RoleContext.cs b/back/persistance/data/relations/RoleContext.cs new file mode 100644 index 0000000..ffafd1f --- /dev/null +++ b/back/persistance/data/relations/RoleContext.cs @@ -0,0 +1,25 @@ +//using back.DataModels; +//using Microsoft.EntityFrameworkCore; + +//namespace back.persistance.data.relations; + + +//public class RoleContext : DbContext +//{ +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// // Role -> Permissions (muchos-a-muchos) +// modelBuilder.Entity() +// .HasMany(r => r.Permissions) +// .WithMany(p => p.Roles) +// .UsingEntity(j => j.ToTable("RolePermissions")); + +// // Role -> BaseRole (auto-referencial) +// modelBuilder.Entity() +// .HasOne(r => r.BaseRoleModel) +// .WithMany() // Un rol base puede ser heredado por múltiples roles +// .HasForeignKey(r => r.BaseRoleModelId); + +// base.OnModelCreating(modelBuilder); +// } +//} diff --git a/back/persistance/data/relations/RoleRelationEstablisher.cs b/back/persistance/data/relations/RoleRelationEstablisher.cs new file mode 100644 index 0000000..30536a4 --- /dev/null +++ b/back/persistance/data/relations/RoleRelationEstablisher.cs @@ -0,0 +1,30 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class RoleRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.BaseRoleModel).WithMany(p => p.InverseBaseRoleModel).HasForeignKey(d => d.BaseRoleModelId); + + entity.HasMany(d => d.Permissions).WithMany(p => p.Roles) + .UsingEntity>( + "RolePermission", + r => r.HasOne().WithMany() + .HasForeignKey("PermissionId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("RoleId", "PermissionId"); + j.ToTable("RolePermissions"); + }); + }); + } +} diff --git a/back/persistance/data/relations/SeedingDbContext.cs b/back/persistance/data/relations/SeedingDbContext.cs new file mode 100644 index 0000000..cd7bff0 --- /dev/null +++ b/back/persistance/data/relations/SeedingDbContext.cs @@ -0,0 +1,40 @@ +//using back.DataModels; +//using Microsoft.EntityFrameworkCore; + +//namespace back.persistance.data.relations; + +//public class SeedingDbContext : DbContext +//{ +// protected override void OnModelCreating(ModelBuilder modelBuilder) +// { +// // 3. CONFIGURAR RELACIONES +// modelBuilder.Entity() +// .HasMany(r => r.Permissions) +// .WithMany(p => p.Roles) +// .UsingEntity>( +// "RolePermissions", +// j => j.HasOne().WithMany().HasForeignKey("PermissionsId"), +// j => j.HasOne().WithMany().HasForeignKey("RolesId"), +// j => j.HasData( +// // Usuario: VIEW_CONTENT y LIKE_CONTENT +// new { RolesId = "1", PermissionsId = "1" }, +// new { RolesId = "1", PermissionsId = "2" }, + +// // Content Manager: permisos adicionales +// new { RolesId = "2", PermissionsId = "5" }, +// new { RolesId = "2", PermissionsId = "3" }, +// new { RolesId = "2", PermissionsId = "4" }, +// new { RolesId = "2", PermissionsId = "9" }, +// new { RolesId = "2", PermissionsId = "8" }, + +// // Admin: permisos adicionales +// new { RolesId = "3", PermissionsId = "6" }, +// new { RolesId = "3", PermissionsId = "7" }, +// new { RolesId = "3", PermissionsId = "10" } +// ) +// ); + +// // Resto de configuraciones... +// base.OnModelCreating(modelBuilder); +// } +//} diff --git a/back/persistance/data/relations/TagRelationEstablisher.cs b/back/persistance/data/relations/TagRelationEstablisher.cs new file mode 100644 index 0000000..8930983 --- /dev/null +++ b/back/persistance/data/relations/TagRelationEstablisher.cs @@ -0,0 +1,15 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class TagRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasIndex(e => e.Name, "IX_Tags_Name").IsUnique(); + }); + } +} diff --git a/back/persistance/data/relations/UserRelationEstablisher.cs b/back/persistance/data/relations/UserRelationEstablisher.cs new file mode 100644 index 0000000..4c68898 --- /dev/null +++ b/back/persistance/data/relations/UserRelationEstablisher.cs @@ -0,0 +1,32 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.relations; + +public class UserRelationEstablisher : IRelationEstablisher +{ + public void EstablishRelation(ModelBuilder modelBuilder) + { + modelBuilder.Entity(entity => + { + entity.HasOne(d => d.IdNavigation).WithOne(p => p.User) + .HasForeignKey(d => d.Id) + .OnDelete(DeleteBehavior.ClientSetNull); + + entity.HasMany(d => d.Roles).WithMany(p => p.Users) + .UsingEntity>( + "UserRole", + r => r.HasOne().WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.ClientSetNull), + l => l.HasOne().WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.ClientSetNull), + j => + { + j.HasKey("UserId", "RoleId"); + j.ToTable("UserRoles"); + }); + }); + } +} diff --git a/back/persistance/data/repositories/Abstracts/IPersonRepository.cs b/back/persistance/data/repositories/Abstracts/IPersonRepository.cs new file mode 100644 index 0000000..6edbbd0 --- /dev/null +++ b/back/persistance/data/repositories/Abstracts/IPersonRepository.cs @@ -0,0 +1,9 @@ +using back.DataModels; +using DependencyInjector.Lifetimes; +using Transactional.Abstractions.Interfaces; + +namespace back.persistance.data.repositories.Abstracts; + +public interface IPersonRepository : IRepository, IScoped +{ +} \ No newline at end of file diff --git a/back/persistance/data/repositories/Abstracts/IPhotoRepository.cs b/back/persistance/data/repositories/Abstracts/IPhotoRepository.cs new file mode 100644 index 0000000..d890d63 --- /dev/null +++ b/back/persistance/data/repositories/Abstracts/IPhotoRepository.cs @@ -0,0 +1,8 @@ +using back.DataModels; +using DependencyInjector.Lifetimes; +using Transactional.Abstractions.Interfaces; + +namespace back.persistance.data.repositories.Abstracts; + +public interface IPhotoRepository : IRepository, IScoped +{ } diff --git a/back/persistance/data/repositories/Abstracts/IUserRepository.cs b/back/persistance/data/repositories/Abstracts/IUserRepository.cs new file mode 100644 index 0000000..4c7bd6b --- /dev/null +++ b/back/persistance/data/repositories/Abstracts/IUserRepository.cs @@ -0,0 +1,14 @@ +using back.DataModels; +using DependencyInjector.Lifetimes; +using Transactional.Abstractions.Interfaces; + +namespace back.persistance.data.repositories.Abstracts; + +public interface IUserRepository : IRepository, IScoped +{ + Task GetByEmail(string email); + Task GetUserSaltByEmail(string email); + Task Login(string email, string password); + Task ExistsByEmail(string email); + //Task IsContentManager(string userId); +} diff --git a/back/persistance/data/repositories/PersonRepository.cs b/back/persistance/data/repositories/PersonRepository.cs new file mode 100644 index 0000000..422f739 --- /dev/null +++ b/back/persistance/data/repositories/PersonRepository.cs @@ -0,0 +1,10 @@ +using back.DataModels; +using back.persistance.data.repositories.Abstracts; +using Transactional.Implementations.EntityFramework; + +namespace back.persistance.data.repositories; + +public class PersonRepository(DataContext context) : ReadWriteRepository(context), IPersonRepository +{ + // Implement methods specific to Photo repository if needed +} \ No newline at end of file diff --git a/back/persistance/data/repositories/PhotoRepository.cs b/back/persistance/data/repositories/PhotoRepository.cs new file mode 100644 index 0000000..e69aa4d --- /dev/null +++ b/back/persistance/data/repositories/PhotoRepository.cs @@ -0,0 +1,10 @@ +using back.DataModels; +using back.persistance.data.repositories.Abstracts; +using Transactional.Implementations.EntityFramework; + +namespace back.persistance.data.repositories; + +public class PhotoRepository(DataContext context) : ReadWriteRepository(context), IPhotoRepository +{ + // Implement methods specific to Photo repository if needed +} diff --git a/back/persistance/data/repositories/UserRepository.cs b/back/persistance/data/repositories/UserRepository.cs new file mode 100644 index 0000000..11d01d4 --- /dev/null +++ b/back/persistance/data/repositories/UserRepository.cs @@ -0,0 +1,70 @@ +using back.DataModels; +using back.persistance.data.repositories.Abstracts; +using Microsoft.EntityFrameworkCore; +using Transactional.Implementations.EntityFramework; + +namespace back.persistance.data.repositories; + +public class UserRepository(DataContext context) : ReadWriteRepository(context), IUserRepository +{ + public async Task GetByEmail(string email) + { + try + { + if (string.IsNullOrEmpty(email)) return null; + return await Entity.FirstOrDefaultAsync(u => u.Email == email); + } + catch + { + return null; + } + } + + public async Task GetUserSaltByEmail(string email) + { + try + { + if (string.IsNullOrEmpty(email)) return string.Empty; + var user = await Entity.FirstOrDefaultAsync(u => u.Email == email); + return user?.Salt ?? string.Empty; + } + catch + { + return string.Empty; + } + } + + public async Task Login(string email, string password) + { + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null; + try + { + return await Entity.FirstOrDefaultAsync(u => u.Email == email && u.Password == password); + } + catch + { + return null; + } + } + + public async Task ExistsByEmail(string email) + { + try + { + if (string.IsNullOrEmpty(email)) return false; + return await Entity.AnyAsync(u => u.Email == email); + } + catch + { + return false; + } + } + + //public async Task IsContentManager(string userId) + //{ + // var user = await GetById(userId); + // if (user == null) + // return false; + // return user.Roles.Any(role => role.IsContentManager() || role.IsAdmin()); + //} +} diff --git a/back/persistance/data/seeders/ISeeder.cs b/back/persistance/data/seeders/ISeeder.cs new file mode 100644 index 0000000..ceda337 --- /dev/null +++ b/back/persistance/data/seeders/ISeeder.cs @@ -0,0 +1,8 @@ +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.seeders; + +public interface ISeeder +{ + void Seed(ModelBuilder modelBuilder); +} diff --git a/back/persistance/data/seeders/PermissionSeeder.cs b/back/persistance/data/seeders/PermissionSeeder.cs new file mode 100644 index 0000000..638e3e0 --- /dev/null +++ b/back/persistance/data/seeders/PermissionSeeder.cs @@ -0,0 +1,23 @@ +//using back.DataModels; +//using Microsoft.EntityFrameworkCore; + +//namespace back.persistance.data.seeders; + +//public class PermissionSeeder : ISeeder +//{ +// public void Seed(ModelBuilder modelBuilder) +// { +// modelBuilder.Entity().HasData( +// Permission.ViewContentPermission, +// Permission.LikeContentPermission, +// Permission.EditContentPermission, +// Permission.DeleteContentPermission, +// Permission.CreateContentPermission, +// Permission.EditUserPermission, +// Permission.DeleteUserPermission, +// Permission.DisableUserPermission, +// Permission.CreateUserPermission, +// Permission.EditWebConfigPermission +// ); +// } +//} \ No newline at end of file diff --git a/back/persistance/data/seeders/RoleSeeder.cs b/back/persistance/data/seeders/RoleSeeder.cs new file mode 100644 index 0000000..3da6ac9 --- /dev/null +++ b/back/persistance/data/seeders/RoleSeeder.cs @@ -0,0 +1,16 @@ +//using back.DataModels; +//using Microsoft.EntityFrameworkCore; + +//namespace back.persistance.data.seeders; + +//public class RoleSeeder : ISeeder +//{ +// public void Seed(ModelBuilder modelBuilder) +// { +// modelBuilder.Entity().HasData( +// new Role { Id = "1", Name = "User", Description = "Role for regular users", BaseRoleModelId = null }, +// new Role { Id = "2", Name = "Content Manager", Description = "Role for managing content", BaseRoleModelId = "1" }, +// new Role { Id = "3", Name = "Admin", Description = "Administrator role with full permissions", BaseRoleModelId = "2" } +// ); +// } +//} \ No newline at end of file diff --git a/back/persistance/data/seeders/SystemUserSeeder.cs b/back/persistance/data/seeders/SystemUserSeeder.cs new file mode 100644 index 0000000..b5cb27e --- /dev/null +++ b/back/persistance/data/seeders/SystemUserSeeder.cs @@ -0,0 +1,14 @@ +using back.DataModels; +using Microsoft.EntityFrameworkCore; + +namespace back.persistance.data.seeders; + +public class SystemUserSeeder : ISeeder +{ + public void Seed(ModelBuilder modelBuilder) + { + modelBuilder.Entity().HasData( + User.SystemUser + ); + } +} \ No newline at end of file diff --git a/back/services/ImageResizer/IImageResizer.cs b/back/services/ImageResizer/IImageResizer.cs deleted file mode 100644 index 5d5c7be..0000000 --- a/back/services/ImageResizer/IImageResizer.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace back.services.ImageResizer; - -public interface IImageResizer -{ - Task ResizeImage(IFormFile image, int v); -} diff --git a/back/services/bussines/Errors.cs b/back/services/bussines/Errors.cs new file mode 100644 index 0000000..866a7cd --- /dev/null +++ b/back/services/bussines/Errors.cs @@ -0,0 +1,11 @@ +using System.Net; + +namespace back.services.bussines; + +public static class Errors +{ + public static readonly HttpErrorMap Unauthorized = + new(HttpStatusCode.Unauthorized, "Invalid user data. Email or password are wrong."); + public static readonly HttpErrorMap BadRequest = + new(HttpStatusCode.BadRequest, "Missing user data."); +} \ No newline at end of file diff --git a/back/services/bussines/HttpErrorMap.cs b/back/services/bussines/HttpErrorMap.cs new file mode 100644 index 0000000..223d084 --- /dev/null +++ b/back/services/bussines/HttpErrorMap.cs @@ -0,0 +1,5 @@ +using System.Net; + +namespace back.services.bussines; + +public record HttpErrorMap(HttpStatusCode Code, string Description); diff --git a/back/services/bussines/PhotoService/IPhotoService.cs b/back/services/bussines/PhotoService/IPhotoService.cs new file mode 100644 index 0000000..80c24f9 --- /dev/null +++ b/back/services/bussines/PhotoService/IPhotoService.cs @@ -0,0 +1,16 @@ +using back.DataModels; +using back.DTO; +using DependencyInjector.Abstractions.ClassTypes; +using DependencyInjector.Lifetimes; + +namespace back.services.bussines.PhotoService; + +public interface IPhotoService: IScoped +{ + Task Create(PhotoFormModel form); + Task Delete(string id, string userId = "00000000-0000-0000-0000-000000000001"); + Task Get(string id, string userId = "00000000-0000-0000-0000-000000000001"); + Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = ""); + Task<(int totalItems, IEnumerable? pageData)> GetPage(int page, int pageSize); + Task Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001"); +} \ No newline at end of file diff --git a/back/services/bussines/PhotoService/PhotoService.cs b/back/services/bussines/PhotoService/PhotoService.cs new file mode 100644 index 0000000..215c3c2 --- /dev/null +++ b/back/services/bussines/PhotoService/PhotoService.cs @@ -0,0 +1,82 @@ +using back.DataModels; +using back.DTO; +using back.persistance.blob; +using back.persistance.data.repositories.Abstracts; + +namespace back.services.bussines.PhotoService; + +public class PhotoService( + IPhotoRepository photoRepository, + IUserRepository userRepository, + IBlobStorageService blobStorageService + ) : IPhotoService +{ + public async Task Create(PhotoFormModel form) + { + ArgumentNullException.ThrowIfNull(form); + if (form.Image == null || form.Image.Length == 0) + throw new ArgumentException("No image uploaded.", nameof(form)); + //if (string.IsNullOrEmpty(form.UserId) || await userRepository.IsContentManager(form.UserId)) + // throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(form.UserId)); + + + + throw new NotImplementedException(); + } + + public async Task Delete(string id, string userId = User.SystemUserId) + { + //if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId)) + // throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId)); + photoRepository.Delete(id); + } + + public async Task Get(string id, string userId = User.SystemUserId) + { + Photo? photo = await photoRepository.GetById(id); + return photo; + //return photo?.CanBeSeenBy(userId) ?? false + // ? photo + // : null; + } + + public async Task<(string? mediaType, byte[]? fileBytes)> GetBytes(string id, string res = "") + { + var photo = await photoRepository.GetById(id); + if (photo == null) + return (null, null); + + string filePath = res.ToLower() switch + { + "high" => photo.HighResUrl, + "mid" => photo.MidResUrl, + "low" or _ => photo.LowResUrl + }; + + string? mediaType = res.ToLower() switch + { + "high" => $"image/{photo.Extension}", + "mid" or "low" or _ => "image/webp", + }; + + return ( + mediaType, + await blobStorageService.GetBytes(filePath) ?? throw new FileNotFoundException("File not found.", filePath) + ); + } + + public async Task<(int totalItems, IEnumerable? pageData)> GetPage(int page, int pageSize) + { + return ( + totalItems: await photoRepository.GetTotalItems(), + pageData: photoRepository.GetPage(page, pageSize) + ); + } + + public async Task Update(Photo photo, string userId = "00000000-0000-0000-0000-000000000001") + { + //if (string.IsNullOrEmpty(userId) || await userRepository.IsContentManager(userId)) + // throw new ArgumentException("Invalid user ID or user is not a content manager.", nameof(userId)); + return await photoRepository.Update(photo); + } +} diff --git a/back/services/bussines/UserService/IUserService.cs b/back/services/bussines/UserService/IUserService.cs new file mode 100644 index 0000000..76dc8e3 --- /dev/null +++ b/back/services/bussines/UserService/IUserService.cs @@ -0,0 +1,14 @@ +using back.DataModels; +using DependencyInjector.Abstractions.ClassTypes; +using DependencyInjector.Lifetimes; + +namespace back.services.bussines.UserService; + +public interface IUserService: IScoped +{ + Task Create(string clientId, User user); + Task Login(string email, string password, string clientId); + Task SendResetPassword(string email); + Task Update(User user); + Task ValidateSystemUser(string email, string password, string systemKey, string clientId); +} \ No newline at end of file diff --git a/back/services/bussines/UserService/UserService.cs b/back/services/bussines/UserService/UserService.cs new file mode 100644 index 0000000..d2ea61a --- /dev/null +++ b/back/services/bussines/UserService/UserService.cs @@ -0,0 +1,133 @@ +using back.DataModels; +using back.persistance.blob; +using back.persistance.data.repositories.Abstracts; +using back.services.engine.Crypto; +using back.services.engine.mailing; +using System.Text; + +namespace back.services.bussines.UserService; + +public class UserService( + IUserRepository userRepository, ICryptoService cryptoService, + IEmailService emailService, + IBlobStorageService blobStorageService + ) : IUserService +{ + private readonly IUserRepository _repository = userRepository ?? throw new ArgumentNullException(nameof(userRepository)); + private readonly ICryptoService _cryptoService = cryptoService; + private readonly IEmailService _emailService = emailService; + private readonly IBlobStorageService _blobStorageService = blobStorageService; + + public async Task Create(string clientId, User user) + { + ArgumentNullException.ThrowIfNull(user); + + if (user.Id != null && await _repository.Exists(user.Id)) + { + return await _repository.GetById(user.Id); + } + if (string.IsNullOrEmpty(user.Email) || string.IsNullOrEmpty(user.Password)) + { + return null; + } + if (await _repository.Exists(user.Email)) + { + return await _repository.GetByEmail(user.Email); + } + + if (string.IsNullOrEmpty(user.Salt)) + { + user.Salt = _cryptoService.Salt(); + } + user.Password = _cryptoService.Decrypt(clientId, user.Password) ?? string.Empty; + user.Password = _cryptoService.HashPassword(user.Password, user.Salt) ?? string.Empty; + + user.CreatedAt = DateTimeOffset.UtcNow.ToString("dd-MM-yyyy HH:mm:ss zz"); + + //user.Roles.Add(Role.UserRole); + + await _repository.Insert(user); + await _repository.SaveChanges(); + return user; + } + + public async Task Update(User user) + { + ArgumentNullException.ThrowIfNull(user); + if (user.Id == null || !await _repository.Exists(user.Id)) + { + return null; + } + var existingUser = await _repository.GetById(user.Id); + if (existingUser == null) return null; + existingUser.Email = user.Email; + await _repository.Update(existingUser); + await _repository.SaveChanges(); + return existingUser; + } + + public async Task Login(string email, string password, string clientId) + { + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password)) return null; + + try + { + var decryptedPass = _cryptoService.Decrypt(clientId, password); + var salt = await _repository.GetUserSaltByEmail(email); + var hashedPassword = _cryptoService.HashPassword(decryptedPass, salt); + var user = await _repository.Login(email, hashedPassword ?? string.Empty); + return user; + } + catch + { + return null; + } + } + + public async Task SendResetPassword(string email) + { + var exists = await _repository.ExistsByEmail(email); + if (!exists) + { + return; + } + await _emailService.SendEmailAsync( + tos: email, + from: "admin@mmorales.photo", + subject: "Reset Password", + body: "If you received this email, it means that you have requested a password reset. Please follow the instructions in the email to reset your password." + ); + } + + public async Task ValidateSystemUser(string email, string password, string systemKey, string clientId) + { + password = _cryptoService.Decrypt(clientId, password) ?? string.Empty; + systemKey = _cryptoService.Decrypt(clientId, systemKey) ?? string.Empty; + if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(password) || string.IsNullOrEmpty(systemKey)) + { + return null; + } + if (!email.Equals("@system", StringComparison.InvariantCultureIgnoreCase)) + { + return null; + } + + var systemKeyBytes = await _blobStorageService.GetBytes("systemkey.lock"); + var systemKeyString = Encoding.UTF8.GetString(systemKeyBytes ?? []); + var systemKeyObject = System.Text.Json.JsonSerializer.Deserialize(systemKeyString); + if (systemKeyObject == null || !systemKeyObject.IsValid(email, password, systemKey)) + { + return null; + } + if (!await _repository.ExistsByEmail(email)) + { + return null; + } + var user = await _repository.GetByEmail(email); + if (user == null) + { + return null; + } + return await Login(user.Email!, user.Password!, clientId); + } +} diff --git a/back/services/Crypto/CryptoService.cs b/back/services/engine/Crypto/CryptoService.cs similarity index 88% rename from back/services/Crypto/CryptoService.cs rename to back/services/engine/Crypto/CryptoService.cs index 29fc1e8..39298a7 100644 --- a/back/services/Crypto/CryptoService.cs +++ b/back/services/engine/Crypto/CryptoService.cs @@ -1,7 +1,7 @@ using Microsoft.Extensions.Caching.Memory; using System.Security.Cryptography; -namespace back.services.Crypto; +namespace back.services.engine.Crypto; public class CryptoService(IMemoryCache cache) : ICryptoService { @@ -28,7 +28,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService } }; - public string? Encrypt(string plainText, string clientId) + public string? Encrypt(string clientId,string plainText) { // get keys from cache if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert)) @@ -41,7 +41,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService } // import rsa keys and configure RSA for encryption using var rsa = RSA.Create(2048); - rsa.ImportRSAPublicKey(Convert.FromBase64String(publicCert), out _); + rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _); rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _); // Encrypt the plain text using RSA string? encryptedText = null; @@ -59,7 +59,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService return encryptedText; } - public string? Decrypt(string encryptedText, string clientId) + public string? Decrypt(string clientId, string encryptedText) { // get keys from cache if (!_cache.TryGetValue($"{clientId}_private", out string? privateCert) || string.IsNullOrEmpty(privateCert)) @@ -72,7 +72,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService } // import rsa keys and configure RSA for decryption using var rsa = RSA.Create(2048); - rsa.ImportRSAPublicKey(Convert.FromBase64String(publicCert), out _); + rsa.ImportSubjectPublicKeyInfo(Convert.FromBase64String(publicCert), out _); rsa.ImportRSAPrivateKey(Convert.FromBase64String(privateCert), out _); // Decrypt the encrypted text using RSA string? plainText = null; @@ -96,8 +96,9 @@ public class CryptoService(IMemoryCache cache) : ICryptoService { return publicCert; } - (publicCert, _) = GenerateCertificate(); + (publicCert, string privateCert) = GenerateCertificate(); _cache.Set($"{clientId}_public", publicCert, _CacheOptions); + _cache.Set($"{clientId}_private", privateCert, _CacheOptions); return publicCert; } @@ -107,7 +108,8 @@ public class CryptoService(IMemoryCache cache) : ICryptoService { return privateCert; } - (_, privateCert) = GenerateCertificate(); + (string publicCert, privateCert) = GenerateCertificate(); + _cache.Set($"{clientId}_public", publicCert, _CacheOptions); _cache.Set($"{clientId}_private", privateCert, _CacheOptions); return privateCert; } @@ -116,7 +118,7 @@ public class CryptoService(IMemoryCache cache) : ICryptoService { // Generate a new RSA key pair for the client using var rsa = RSA.Create(2048); - var publicKey = rsa.ExportRSAPublicKey(); + var publicKey = rsa.ExportSubjectPublicKeyInfo(); var privateKey = rsa.ExportRSAPrivateKey(); // Convert to Base64 strings for storage var publicCert = Convert.ToBase64String(publicKey); @@ -165,4 +167,9 @@ public class CryptoService(IMemoryCache cache) : ICryptoService rng.GetBytes(saltBytes); return Convert.ToBase64String(saltBytes); } + + public string? HashPassword(string plainPassword, string plainSalt) + { + return Hash($"{plainPassword}{plainSalt}{Pepper()}"); + } } \ No newline at end of file diff --git a/back/services/Crypto/ICryptoService.cs b/back/services/engine/Crypto/ICryptoService.cs similarity index 64% rename from back/services/Crypto/ICryptoService.cs rename to back/services/engine/Crypto/ICryptoService.cs index 4d0e757..2a957fd 100644 --- a/back/services/Crypto/ICryptoService.cs +++ b/back/services/engine/Crypto/ICryptoService.cs @@ -1,10 +1,13 @@ -namespace back.services.Crypto; +using DependencyInjector.Lifetimes; -public interface ICryptoService +namespace back.services.engine.Crypto; + +public interface ICryptoService : ISingleton { string? Encrypt(string clientId, string plainText); string? Decrypt(string clientId, string encryptedText); string? Hash(string plainText); + string? HashPassword(string? plainPassword, string? plainSalt); bool VerifyHash(string plainText, string hash); string Salt(); string Pepper(); diff --git a/back/services/engine/ImageResizer/IImageResizer.cs b/back/services/engine/ImageResizer/IImageResizer.cs new file mode 100644 index 0000000..dbb82a2 --- /dev/null +++ b/back/services/engine/ImageResizer/IImageResizer.cs @@ -0,0 +1,8 @@ +using DependencyInjector.Lifetimes; + +namespace back.services.engine.ImageResizer; + +public interface IImageResizer : ISingleton +{ + Task ResizeImage(IFormFile image, int v); +} diff --git a/back/services/ImageResizer/ImageResizer.cs b/back/services/engine/ImageResizer/ImageResizer.cs similarity index 94% rename from back/services/ImageResizer/ImageResizer.cs rename to back/services/engine/ImageResizer/ImageResizer.cs index 210656e..b45edce 100644 --- a/back/services/ImageResizer/ImageResizer.cs +++ b/back/services/engine/ImageResizer/ImageResizer.cs @@ -1,7 +1,7 @@ using SixLabors.ImageSharp; using SixLabors.ImageSharp.Processing; -namespace back.services.ImageResizer; +namespace back.services.engine.ImageResizer; public sealed class ImageResizer : IImageResizer { diff --git a/back/services/engine/PasswordGenerator/IPasswordGenerator.cs b/back/services/engine/PasswordGenerator/IPasswordGenerator.cs new file mode 100644 index 0000000..aef6e6c --- /dev/null +++ b/back/services/engine/PasswordGenerator/IPasswordGenerator.cs @@ -0,0 +1,8 @@ +using DependencyInjector.Lifetimes; + +namespace back.services.engine.PasswordGenerator; + +public interface IPasswordGenerator : ISingleton +{ + string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true); +} \ No newline at end of file diff --git a/back/services/engine/PasswordGenerator/PasswordGenerator.cs b/back/services/engine/PasswordGenerator/PasswordGenerator.cs new file mode 100644 index 0000000..b4eb044 --- /dev/null +++ b/back/services/engine/PasswordGenerator/PasswordGenerator.cs @@ -0,0 +1,24 @@ +namespace back.services.engine.PasswordGenerator; + +public class PasswordGenerator : IPasswordGenerator +{ + public string Generate(int length, bool includeNumbers = true, bool includeMayus = true, bool includeMinus = true, bool includeSpecials = true) + { + const string numbers = "0123456789"; + const string mayus = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + const string minus = "abcdefghijklmnopqrstuvwxyz"; + const string specials = "!@#$%^&*()_+[]{}|;:,.<>?"; + var characters = minus; + if (includeNumbers) characters += numbers; + if (includeMayus) characters += mayus; + if (includeSpecials) characters += specials; + var random = new Random((int)DateTimeOffset.UtcNow.Ticks); + var password = new char[length]; + + for (int i = 0; i < length; i++) + { + password[i] = characters[random.Next(characters.Length)]; + } + return new string(password); + } +} diff --git a/back/services/engine/SystemUser/ISystemUserGenerator.cs b/back/services/engine/SystemUser/ISystemUserGenerator.cs new file mode 100644 index 0000000..c148ec4 --- /dev/null +++ b/back/services/engine/SystemUser/ISystemUserGenerator.cs @@ -0,0 +1,8 @@ +using DependencyInjector.Lifetimes; + +namespace back.services.engine.SystemUser; + +public interface ISystemUserGenerator: IScoped +{ + Task GenerateAsync(); +} \ No newline at end of file diff --git a/back/services/engine/SystemUser/SystemUserGenerator.cs b/back/services/engine/SystemUser/SystemUserGenerator.cs new file mode 100644 index 0000000..605f3c1 --- /dev/null +++ b/back/services/engine/SystemUser/SystemUserGenerator.cs @@ -0,0 +1,47 @@ +using back.DataModels; +using back.persistance.blob; +using back.persistance.data; +using back.persistance.data.repositories.Abstracts; +using back.services.engine.Crypto; +using back.services.engine.PasswordGenerator; +using Transactional.Abstractions.Interfaces; + +namespace back.services.engine.SystemUser; + +public class SystemUserGenerator( + ITransactionalService transactional, + IUserRepository userRepository, + IPersonRepository personRepository, + ICryptoService cryptoService, + IBlobStorageService blobStorageService, + IPasswordGenerator passwordGenerator) : ISystemUserGenerator +{ + public async Task GenerateAsync() + { + var systemKey = new SystemKey() { + Password = passwordGenerator.Generate(16), + }; + var systemKeyJson = System.Text.Json.JsonSerializer.Serialize(systemKey); + + using Stream stream = new MemoryStream(System.Text.Encoding.UTF8.GetBytes(systemKeyJson)); + + await blobStorageService.Delete("systemkey.lock"); + + await blobStorageService.Save( + stream, + "systemkey.lock" + ); + + User.SystemUser.Password = systemKey.Password; + User.SystemUser.Salt = cryptoService.Salt(); + User.SystemUser.Password = cryptoService.HashPassword(User.SystemUser.Password, User.SystemUser.Salt) ?? string.Empty; + + if (!await userRepository.Exists(User.SystemUser.Id!)) + { + await transactional.DoTransaction(async () => { + await personRepository.Insert(Person.SystemPerson); + await userRepository.Insert(User.SystemUser); + }); + } + } +} \ No newline at end of file diff --git a/back/services/engine/mailing/EmailService.cs b/back/services/engine/mailing/EmailService.cs new file mode 100644 index 0000000..bdc1089 --- /dev/null +++ b/back/services/engine/mailing/EmailService.cs @@ -0,0 +1,66 @@ +using back.Options; +using Microsoft.Extensions.Options; +using System.Net; +using System.Net.Mail; + +namespace back.services.engine.mailing; + +public class EmailService(IOptions options) : IEmailService +{ + public async Task SendEmailAsync(List tos, string from, string subject, string body, Dictionary? attachments = null, CancellationToken cancellationToken = default) + { + try + { + await Parallel.ForEachAsync(tos, async (to, cancellationToken) => { + await SendEmailAsync(to, from, subject, body, attachments, cancellationToken); + }); + } + catch (Exception ex) + { + // Log the exception or handle it as needed + Console.WriteLine($"Error sending email to multiple recipients: {ex.Message}"); + } + } + public async Task SendEmailAsync(string to, string from, string subject, string body, Dictionary? attachments = null, CancellationToken cancellationToken = default) + { + try + { + using var message = new MailMessage(); + message.From = new MailAddress(from); + message.To.Add(to); + message.Subject = subject; + message.Body = body; + message.IsBodyHtml = true; + message.Priority = MailPriority.Normal; + message.DeliveryNotificationOptions = DeliveryNotificationOptions.Never; + + if (attachments != null) + { + foreach (var attachment in attachments) + { + if (attachment.Value is FileStream fileStream) + { + message.Attachments.Add(new Attachment(fileStream, attachment.Key)); + } + if (attachment.Value is string filePath && File.Exists(filePath)) + { + message.Attachments.Add(new Attachment(filePath)); + } + } + } + + using var cliente = new SmtpClient(options.Value.SmtpServer, options.Value.Puerto); + cliente.UseDefaultCredentials = false; + cliente.Credentials = new NetworkCredential(options.Value.Usuario, options.Value.Password); + cliente.EnableSsl = options.Value.EnableSsl; + cliente.DeliveryMethod = SmtpDeliveryMethod.Network; + + await cliente.SendMailAsync(message, cancellationToken); + } + catch (Exception ex) + { + // Log the exception or handle it as needed + Console.WriteLine($"Error sending email: {ex.Message}"); + } + } +} diff --git a/back/services/engine/mailing/IEmailService.cs b/back/services/engine/mailing/IEmailService.cs new file mode 100644 index 0000000..b8242bf --- /dev/null +++ b/back/services/engine/mailing/IEmailService.cs @@ -0,0 +1,9 @@ +using DependencyInjector.Lifetimes; + +namespace back.services.engine.mailing; + +public interface IEmailService : IScoped +{ + Task SendEmailAsync(List tos, string from, string subject, string body, Dictionary? attachments = null, CancellationToken cancellationToken = default); + Task SendEmailAsync(string tos, string from, string subject, string body, Dictionary? attachments = null, CancellationToken cancellationToken = default); +} diff --git a/front/v2/src/app/services/crypto-service/crypto-service.ts b/front/v2/src/app/services/crypto-service/crypto-service.ts index eb9c5cd..2360e2e 100644 --- a/front/v2/src/app/services/crypto-service/crypto-service.ts +++ b/front/v2/src/app/services/crypto-service/crypto-service.ts @@ -22,11 +22,7 @@ export class CryptoService { private async fetchPublicKey(): Promise { try { - const response = await axios.get(`${this.cryptoUrl}`, { - headers: { - 'X-Thumbprint': this.thumbprintService.getThumbprint(), - }, - }); + const response = await axios.get(`${this.cryptoUrl}`); if (response.status !== 200) { throw new Error('Failed to fetch public key'); @@ -39,7 +35,7 @@ export class CryptoService { binaryDer, { name: 'RSA-OAEP', - hash: { name: 'SHA-256' }, + hash: 'SHA-256', }, false, ['encrypt'] diff --git a/front/v2/src/app/services/userService/userService.ts b/front/v2/src/app/services/userService/userService.ts index d027f4e..c69fa11 100644 --- a/front/v2/src/app/services/userService/userService.ts +++ b/front/v2/src/app/services/userService/userService.ts @@ -22,6 +22,28 @@ export class userService { return this.userSubject.asObservable(); } + async systemLogin( + email: string | null | undefined, + password: string | null | undefined, + systemKey: string | null | undefined + ) { + if (email == null || password == null || systemKey == null) { + return; + } + const encryptedPassword = this.cryptoService.encryptData(password); + const encryptedSystemKey = this.cryptoService.encryptData(systemKey); + const response = await axios.post('/users/login', { + email, + password: await encryptedPassword, + systemKey: await encryptedSystemKey, + }); + const { jwt, refresh, usermodel } = response.data; + localStorage.setItem('jwt', jwt); + localStorage.setItem('refresh', refresh); + this.setUser(usermodel); + return usermodel; + } + async login( email: string | null | undefined, password: string | null | undefined @@ -40,4 +62,25 @@ export class userService { this.setUser(usermodel); return usermodel; } + + async register( + name: string | null | undefined, + email: string | null | undefined, + password: string | null | undefined + ): Promise { + if (email == null || password == null) { + throw new Error('Email and password must not be null'); + } + const encrypted = this.cryptoService.encryptData(password); + const response = await axios.post('/users/register', { + name, + email, + password: await encrypted, + }); + const { jwt, refresh, usermodel } = response.data; + localStorage.setItem('jwt', jwt); + localStorage.setItem('refresh', refresh); + this.setUser(usermodel); + return usermodel; + } } diff --git a/front/v2/src/app/views/forgot-password-view/forgot-password-view.html b/front/v2/src/app/views/forgot-password-view/forgot-password-view.html index f2a39f0..42354a5 100644 --- a/front/v2/src/app/views/forgot-password-view/forgot-password-view.html +++ b/front/v2/src/app/views/forgot-password-view/forgot-password-view.html @@ -1 +1,29 @@ -

forgot-password-view works!

+
+

Recuperar contraseña

+ @if(showingForm()) { +
+
+ + + + + +
+
+ +
+
+ } @else{ +
+

+ Si existe un usuario con ese correo electrónico, se enviará un enlace para + iniciar sesión y restablecer la contraseña. +

+
+ } +
diff --git a/front/v2/src/app/views/forgot-password-view/forgot-password-view.scss b/front/v2/src/app/views/forgot-password-view/forgot-password-view.scss index e69de29..027a5b6 100644 --- a/front/v2/src/app/views/forgot-password-view/forgot-password-view.scss +++ b/front/v2/src/app/views/forgot-password-view/forgot-password-view.scss @@ -0,0 +1,18 @@ +.forgot-password-form-container { + max-width: 400px; + margin: 0 auto; + padding: 20px; + border: 1px solid #ccc; + border-radius: 5px; + background-color: #fff; +} + +.forgot-password-form { + display: flex; + flex-direction: column; +} + +.form-buttons { + display: flex; + justify-content: flex-end; +} diff --git a/front/v2/src/app/views/forgot-password-view/forgot-password-view.ts b/front/v2/src/app/views/forgot-password-view/forgot-password-view.ts index 94829fd..a8c11bd 100644 --- a/front/v2/src/app/views/forgot-password-view/forgot-password-view.ts +++ b/front/v2/src/app/views/forgot-password-view/forgot-password-view.ts @@ -1,11 +1,37 @@ -import { Component } from '@angular/core'; +import { Component, inject, signal } from '@angular/core'; +import { SvgButton } from '../../../utils/svg-button/svg-button'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { emailValidator } from '../../../utils/validators/emailValidator'; +import axios from 'axios'; @Component({ selector: 'forgot-password-view', - imports: [], + imports: [SvgButton, ReactiveFormsModule], templateUrl: './forgot-password-view.html', - styleUrl: './forgot-password-view.scss' + styleUrl: './forgot-password-view.scss', }) export class ForgotPasswordView { + private formBuilder = inject(FormBuilder); + form = this.formBuilder.group({ + email: ['', [Validators.required, emailValidator]], + }); + showingForm = signal(true); + get email() { + return this.form.get('email'); + } + + hasEmailContent(): boolean { + const emailValue = this.email?.value; + return emailValue ? emailValue.trim().length > 0 : false; + } + + async onSubmit() { + if (this.form.valid) { + this.showingForm.set(false); + await axios.post('/users/forgot-password', { + email: this.email?.value, + }); + } + } } diff --git a/front/v2/src/app/views/login-view/login-view.html b/front/v2/src/app/views/login-view/login-view.html index 48a479a..0d61b81 100644 --- a/front/v2/src/app/views/login-view/login-view.html +++ b/front/v2/src/app/views/login-view/login-view.html @@ -6,7 +6,11 @@

Acceder

@if(submitError()) {
{{ submitError() }}
- } + @if(showRegisterLink()) { + + } }
@@ -108,6 +126,7 @@ text="Más opciones de inicio de sesión" (click)="onMoreLoginOptions()" > + @if(!showRegisterLink()) { + }
} diff --git a/front/v2/src/app/views/login-view/login-view.scss b/front/v2/src/app/views/login-view/login-view.scss index 77d57f1..06b41e0 100644 --- a/front/v2/src/app/views/login-view/login-view.scss +++ b/front/v2/src/app/views/login-view/login-view.scss @@ -7,14 +7,14 @@ align-items: center; justify-content: center; width: 20%; - padding-top: 5rem; + padding-top: 2rem; margin: 0 auto; gap: 1.5rem; .login-form { display: flex; flex-direction: column; - gap: 1rem; + gap: 1.5rem; width: 100%; } @@ -46,6 +46,13 @@ } } + .login-buttons { + display: flex; + flex-direction: column; + gap: 1rem; + width: 100%; + } + .provider-buttons { display: grid; grid-template-columns: 1fr 1fr; diff --git a/front/v2/src/app/views/login-view/login-view.ts b/front/v2/src/app/views/login-view/login-view.ts index 8baa9c9..3af1b62 100644 --- a/front/v2/src/app/views/login-view/login-view.ts +++ b/front/v2/src/app/views/login-view/login-view.ts @@ -6,7 +6,7 @@ import { Router } from '@angular/router'; import { emailValidator } from '../../../utils/validators/emailValidator'; import { PasswordValidator } from '../../../utils/validators/passwordValidator'; import { emailPasswordDistinctValidator } from '../../../utils/validators/distinctEmailPasswordValidator'; -import { from, single } from 'rxjs'; +import { from } from 'rxjs'; @Component({ selector: 'login-view', @@ -19,18 +19,20 @@ export class LoginView { private formBuilder = inject(FormBuilder); private router = inject(Router); private timer: any; - private cuentaAtras: number = 30; - private disableLogin = signal(false); - private entrando = signal(false); - private submitError = signal(null); + cuentaAtras: number = 30; + disableLogin = signal(false); + entrando = signal(false); + submitError = signal(null); + showRegisterLink = signal(false); - private loginForm = this.formBuilder.group( + loginForm = this.formBuilder.group( { email: ['', [Validators.required, emailValidator]], password: [ '', [Validators.required, Validators.minLength(8), PasswordValidator], ], + systemKey: [''], }, { validators: emailPasswordDistinctValidator } ); @@ -43,6 +45,10 @@ export class LoginView { return this.loginForm.get('password'); } + get systemKey() { + return this.loginForm.get('systemKey'); + } + // Métodos para verificar si los campos tienen contenido hasEmailContent(): boolean { const emailValue = this.email?.value; @@ -72,6 +78,19 @@ export class LoginView { : false; } + isLoginSystem(): boolean { + const emailValue = this.email?.value; + if (emailValue && emailValue == '@system') { + return true; + } + return false; + } + + hasSystemKeyContent(): boolean { + const systemKeyValue = this.systemKey?.value; + return systemKeyValue ? systemKeyValue.trim().length > 0 : false; + } + ngOnDestroy() { this.submitError.set(null); this.limpiarTimer(); @@ -101,10 +120,20 @@ export class LoginView { } onSubmit() { - if (this.loginForm.valid) { + const email = this.loginForm.value.email; + const password = this.loginForm.value.password; + if (this.isLoginSystem()) { + const systemKey = this.systemKey?.value; + from(this.userService.systemLogin(email, password, systemKey)).subscribe({ + next: (user) => { + this.router.navigate(['/']); + }, + error: (error) => { + this.router.navigate(['/']); + }, + }); + } else if (this.loginForm.valid) { this.entrando.set(true); - const email = this.loginForm.value.email; - const password = this.loginForm.value.password; from(this.userService.login(email, password)).subscribe({ next: (user) => { this.router.navigate(['/']); @@ -131,9 +160,8 @@ export class LoginView { 'No se ha podido conectar con el servidor. Vuelva a intentarlo más tarde. Reconectando...' ); } else if (error.status === 404) { - this.submitError.set( - 'El usuario nunca ha existido. ¿Quieres registrarte?' - ); + this.submitError.set('El usuario nunca ha existido.'); + this.showRegisterLink.set(true); } }, }); diff --git a/front/v2/src/app/views/register-view/register-view.html b/front/v2/src/app/views/register-view/register-view.html index 92fb714..4cf4ee4 100644 --- a/front/v2/src/app/views/register-view/register-view.html +++ b/front/v2/src/app/views/register-view/register-view.html @@ -1 +1,83 @@ -

register-view works!

+
+
+
+ + + + + +
+
+ + + + + +
+ @if(emailHasErrors()) { +
+ * El email no es válido. + @if(this.email?.hasError('email')) { + El email no tiene un formato válido. + } +
+ } +
+ + + + + +
+ @if(passwordHasErrors()) { +
+ * La contraseña no es válida.
+ @if(this.password?.hasError('minlength')) { + Debe tener al menos 8 caracteres. + } @else { @if(this.password?.hasError('passwordContainsEmailOrLeet')) { + No puede contener trazas del email. + } @if(this.password?.hasError('invalidPassword')) { + + No tiene un formato válido.
+ Tiene que contener al menos una letra mayúscula, una letra minúscula, un + número y un carácter especial. +
+ } } +
+ } +
+ + + + + +
+ @if(confirmPasswordHasErrors()) { +
+ * Las dos contraseñas tienen que coincidir. +
+ } + + +
+
diff --git a/front/v2/src/app/views/register-view/register-view.scss b/front/v2/src/app/views/register-view/register-view.scss index e69de29..27289a2 100644 --- a/front/v2/src/app/views/register-view/register-view.scss +++ b/front/v2/src/app/views/register-view/register-view.scss @@ -0,0 +1,67 @@ +.register-view { + form { + display: flex; + flex-direction: column; + gap: 1rem; + } + + .text-input { + position: relative; + + input { + width: 100%; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; + + &:focus { + border-color: #007bff; + outline: none; + } + } + + label { + position: absolute; + top: 0.5rem; + left: 0.5rem; + transition: 0.2s; + } + + &.has-content { + label { + top: -1rem; + left: 0.5rem; + font-size: 0.8rem; + color: #007bff; + } + } + + .focus-border { + position: absolute; + bottom: 0; + left: 0; + right: 0; + height: 2px; + background: #007bff; + transform: scaleX(0); + transition: transform 0.2s; + } + + &:focus-within .focus-border { + transform: scaleX(1); + } + } + + button[type="submit"] { + padding: 0.5rem 1rem; + border: none; + border-radius: 4px; + background: #007bff; + color: white; + cursor: pointer; + + &:hover { + background: darken(#007bff, 10%); + } + } +} diff --git a/front/v2/src/app/views/register-view/register-view.ts b/front/v2/src/app/views/register-view/register-view.ts index 76c1692..e0ea488 100644 --- a/front/v2/src/app/views/register-view/register-view.ts +++ b/front/v2/src/app/views/register-view/register-view.ts @@ -1,11 +1,139 @@ -import { Component } from '@angular/core'; +import { Component, inject } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { emailValidator } from '../../../utils/validators/emailValidator'; +import { SigningMethods, userModel } from '../../../models/userModel'; +import { emailPasswordDistinctValidator } from '../../../utils/validators/distinctEmailPasswordValidator'; +import { from } from 'rxjs'; +import { Router } from '@angular/router'; +import { userService } from '../../services/userService/userService'; @Component({ selector: 'register-view', - imports: [], + imports: [ReactiveFormsModule], templateUrl: './register-view.html', - styleUrl: './register-view.scss' + styleUrl: './register-view.scss', }) export class RegisterView { + private userService = inject(userService); + private formBuilder = inject(FormBuilder); + private router = inject(Router); + form = this.formBuilder.group( + { + name: ['', [Validators.required]], + email: ['', [Validators.required, emailValidator]], + password: ['', [Validators.required, Validators.minLength(6)]], + confirmPassword: [''], + // preferredSigninMethod: [SigningMethods.Password], + // profilePicture: [null], + // bio: ['', [Validators.maxLength(500)]], + // socialMedia: this.formBuilder.group({ + // facebook: [''], + // twitter: [''], + // instagram: [''], + // }), + // termsAccepted: [false, [Validators.requiredTrue]], + }, + { validators: emailPasswordDistinctValidator } + ); + get name() { + return this.form.get('name'); + } + + get email() { + return this.form.get('email'); + } + + get password() { + return this.form.get('password'); + } + + get confirmPassword() { + return this.form.get('confirmPassword'); + } + + get bio() { + return this.form.get('bio'); + } + + get socialMedia() { + return this.form.get('socialMedia'); + } + + get profilePicture() { + return this.form.get('profilePicture'); + } + + get preferredSigninMethod() { + return this.form.get('preferredSigninMethod'); + } + + // getTermsAccepted() { + // return this.form.value.termsAccepted; + // } + + onSubmit() { + if (this.form.valid) { + const email = this.form.value.email; + const password = this.form.value.password; + const name = this.form.value.name; + from(this.userService.register(name, email, password)).subscribe({ + next: (user) => { + this.router.navigate(['/']); + }, + error: (error) => { + console.error('Register error:', error); + }, + }); + } + } + + hasNameContent(): boolean { + const nameValue = this.name?.value; + return nameValue ? nameValue.trim().length > 0 : false; + } + + hasEmailContent(): boolean { + const emailValue = this.email?.value; + return emailValue ? emailValue.trim().length > 0 : false; + } + + hasPasswordContent(): boolean { + const passwordValue = this.password?.value; + return passwordValue ? passwordValue.trim().length > 0 : false; + } + + hasConfirmPasswordContent(): boolean { + const confirmPasswordValue = this.confirmPassword?.value; + return confirmPasswordValue + ? confirmPasswordValue.trim().length > 0 + : false; + } + + emailHasErrors(): boolean { + const emailControl = this.email; + return emailControl + ? this.hasEmailContent() && + emailControl.invalid && + (emailControl.dirty || emailControl.touched) + : false; + } + + passwordHasErrors(): boolean { + const passwordControl = this.password; + return passwordControl + ? this.hasPasswordContent() && + passwordControl.invalid && + (passwordControl.dirty || passwordControl.touched) + : false; + } + + confirmPasswordHasErrors(): boolean { + const confirmPasswordControl = this.confirmPassword; + return confirmPasswordControl + ? this.hasConfirmPasswordContent() && + confirmPasswordControl.invalid && + (confirmPasswordControl.dirty || confirmPasswordControl.touched) + : false; + } } diff --git a/front/v2/src/models/personModel.ts b/front/v2/src/models/personModel.ts index ea53914..10bb67f 100644 --- a/front/v2/src/models/personModel.ts +++ b/front/v2/src/models/personModel.ts @@ -17,6 +17,7 @@ type personModelType = { profilePicture?: string | null; avatar?: string | null; socialMedia?: socialMediaType | null; + bio?: string | null; }; export class personModel { @@ -26,18 +27,21 @@ export class personModel { profilePicture, avatar, socialMedia, + bio, }: personModelType) { this.id = id; this.name = name; this.profilePicture = profilePicture || null; this.avatar = avatar || null; this.socialMedia = socialMedia || null; + this.bio = bio || null; } public id: string; public name: string; public profilePicture: string | null = null; public avatar: string | null = null; + public bio: string | null = null; public socialMedia: { facebook: string | null; instagram: string | null; diff --git a/front/v2/src/models/userModel.ts b/front/v2/src/models/userModel.ts index 1a2902f..1f12567 100644 --- a/front/v2/src/models/userModel.ts +++ b/front/v2/src/models/userModel.ts @@ -1,7 +1,7 @@ import { personModel } from './personModel'; import { roleModel } from './roleModel'; -export enum SigningMethods { +enum SigningMethods { Password = 'password', MagicLink = 'magic-link', Passkeys = 'passkeys', @@ -11,7 +11,7 @@ export enum SigningMethods { Microsoft = 'microsoft', } -export class userModel extends personModel { +class userModel extends personModel { constructor( public override id: string, public email: string, @@ -66,3 +66,5 @@ export class userModel extends personModel { true ); } + +export { SigningMethods, userModel }; diff --git a/front/v2/src/styles/_buttons.scss b/front/v2/src/styles/_buttons.scss index 9fa3a9b..647b731 100644 --- a/front/v2/src/styles/_buttons.scss +++ b/front/v2/src/styles/_buttons.scss @@ -18,10 +18,6 @@ color: $primary-white !important; background-color: $disabled-color !important; border-color: $disabled-color !important; - &:hover { - transform: scale(1); - box-shadow: none; - } } } @@ -37,13 +33,13 @@ } .register-link-button { - color: #1565c0; - background-color: rgba(#1565c0, 0.08); - border-color: #1565c0; + color: #607d8b; + background-color: rgba(#607d8b, 0.08); + border-color: #607d8b; &:hover { color: $primary-white; - background-color: #1565c0; - border-color: #1565c0; + background-color: #607d8b; + border-color: #607d8b; } } diff --git a/front/v2/src/styles/_inputs.scss b/front/v2/src/styles/_inputs.scss index 64b47cd..ab498e1 100644 --- a/front/v2/src/styles/_inputs.scss +++ b/front/v2/src/styles/_inputs.scss @@ -6,6 +6,7 @@ $no-validated: #0603a1; // Example color for effect 20 border .text-input { width: 100%; + font-size: larger; } // Contenedor para el efecto @@ -19,7 +20,7 @@ $no-validated: #0603a1; // Example color for effect 20 border background: transparent; color: $text-dark; width: 100%; - height: 2.5rem; + height: 2.8rem; padding-left: 1rem; box-sizing: border-box; font-size: 1rem; diff --git a/front/v2/src/styles/themes/dark.scss b/front/v2/src/styles/themes/dark.scss new file mode 100644 index 0000000..e69de29 diff --git a/front/v2/src/styles/themes/light.scss b/front/v2/src/styles/themes/light.scss new file mode 100644 index 0000000..e69de29 diff --git a/front/v2/src/utils/svg-button/svg-button.scss b/front/v2/src/utils/svg-button/svg-button.scss index fa82abf..ae13942 100644 --- a/front/v2/src/utils/svg-button/svg-button.scss +++ b/front/v2/src/utils/svg-button/svg-button.scss @@ -2,6 +2,7 @@ @use "../../styles/variables" as *; button { + font-size: larger; background: $primary-white; border: 2px solid $standard-button-border; color: $standard-button-icon; @@ -20,6 +21,19 @@ button { align-items: center; align-content: center; justify-content: space-evenly; + &:disabled { + $disabled-color: rgba( + $color: #999999, + $alpha: 0.45, + ); + color: $primary-white !important; + background-color: $disabled-color !important; + border-color: $disabled-color !important; + &:hover { + transform: scale(1); + box-shadow: none; + } + } label { text-align: center; diff --git a/pruebas/login-form-icons-readme.txt b/pruebas/login-form-icons-readme.txt new file mode 100644 index 0000000..e69de29 diff --git a/pruebas/login-form.css b/pruebas/login-form.css new file mode 100644 index 0000000..e69de29