diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..35207b4 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,399 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Ww][Ii][Nn]32/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.tlog +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# Visual Studio History (VSHistory) files +.vshistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +*.sln.iml +GameDevPortal.WebAPI/appsettings.Development.json diff --git a/backend/GameDevPortal.Core/Entities/Category.cs b/backend/GameDevPortal.Core/Entities/Category.cs new file mode 100644 index 0000000..b8bb5c8 --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/Category.cs @@ -0,0 +1,36 @@ +using GameDevPortal.Core.Extensions; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.Core.Entities; + +public class Category : Entity +{ + [MaxLength(50)] + public string Name { get; private set; } + + [MaxLength(7)] + public string HexColour { get; private set; } + + public Guid? ProjectId { get; private set; } + public IReadOnlyList ProgressReports => _progressReports.AsReadOnly(); + + private readonly List _progressReports = new(); + + public Category(string name, string hexColour, Guid projectId) + { + SetChangableValues(name, hexColour); + ProjectId = projectId; + } + + public void SetChangableValues(string name, string hexColour) + { + name.ThrowIfEmptyOrNull(nameof(name)); + Name = name; + + hexColour.ThrowIfNotHex(nameof(hexColour)); + HexColour = hexColour; + } + + private Category() + { } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/Entity.cs b/backend/GameDevPortal.Core/Entities/Entity.cs new file mode 100644 index 0000000..bea7f5e --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/Entity.cs @@ -0,0 +1,15 @@ +namespace GameDevPortal.Core.Entities; + +public abstract class Entity +{ + public Guid Id { get; protected set; } + public DateTime CreatedAt { get; private set; } + public DateTime UpdatedAt { get; private set; } + public bool IsDeleted { get; private set; } + + protected Entity() + { + Id = Guid.NewGuid(); + IsDeleted = false; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/FAQ.cs b/backend/GameDevPortal.Core/Entities/FAQ.cs new file mode 100644 index 0000000..b6936d8 --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/FAQ.cs @@ -0,0 +1,34 @@ +using GameDevPortal.Core.Extensions; +using System.ComponentModel.DataAnnotations; +using System.Xml.Linq; + +namespace GameDevPortal.Core.Entities; + +public class Faq : Entity +{ + [MaxLength(150)] + public string Question { get; private set; } + [MaxLength(1500)] + public string Answer { get; private set; } + + public Project? Project { get; private set; } + public Guid ProjectId { get; private set; } + + public Faq(string question, string answer) + { + question.ThrowIfEmptyOrNull(nameof(question)); + Question = question; + + answer.ThrowIfEmptyOrNull(nameof(answer)); + Answer = answer; + } + + public void SetChangableValues(string question, string answer) + { + question.ThrowIfEmptyOrNull(nameof(question)); + Question = question; + + answer.ThrowIfNotHex(nameof(answer)); + Answer = answer; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/ProgressReport.cs b/backend/GameDevPortal.Core/Entities/ProgressReport.cs new file mode 100644 index 0000000..39512cb --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/ProgressReport.cs @@ -0,0 +1,74 @@ +using GameDevPortal.Core.Extensions; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.Core.Entities; + +public class ProgressReport : Entity +{ + [MaxLength(150)] + public string Title { get; private set; } + + [MaxLength(500)] + public string Description { get; private set; } + + public IReadOnlyList Categories => _categories.AsReadOnly(); + public IReadOnlyList CategoryIds => _categoryIds.AsReadOnly(); + public IReadOnlyList Files => _files.AsReadOnly(); + public IReadOnlyList FileIds => _fileIds.AsReadOnly(); + public DateTime MadePublicAt { get; private set; } + + public Project? Project { get; private set; } + public Guid ProjectId { get; private set; } + + private readonly List _categories = new List(); + private readonly List _categoryIds = new List(); + private readonly List _files = new List(); + private readonly List _fileIds = new List(); + + public ProgressReport(string title, string description, DateTime madePublicAt, Guid projectId, IEnumerable categoryIds) + { + SetChangableValues(title, description, madePublicAt, categoryIds); + ProjectId = projectId; + } + + public void SetChangableValues(string title, string description, DateTime madePublicAt, IEnumerable categoryIds) + { + title.ThrowIfEmptyOrNull(nameof(title)); + Title = title; + + description.ThrowIfEmptyOrNull(nameof(description)); + Description = description; + + MadePublicAt = madePublicAt; + + _categoryIds.Clear(); + foreach (Guid categoryId in categoryIds) + { + _categoryIds.Add(categoryId); + } + } + + public void AddCategory(Category category) + { + _categories.Add(category); + } + + public void AddCategories(IEnumerable categories) + { + _categories.AddRange(categories); + } + + public void ClearCategories() + { + _categories.Clear(); + } + + public void SyncCategoryIdsToCategories() + { + _categoryIds.Clear(); + _categories.ForEach(c => _categoryIds.Add(c.Id)); + } + + private ProgressReport() + { } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/Project.cs b/backend/GameDevPortal.Core/Entities/Project.cs new file mode 100644 index 0000000..ee490e5 --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/Project.cs @@ -0,0 +1,46 @@ +using GameDevPortal.Core.Extensions; +using GameDevPortal.Core.ValueTypes; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.Core.Entities; + +public class Project : Entity +{ + [MaxLength(100)] + public string Name { get; private set; } + + [MaxLength(500)] + public string Description { get; private set; } + + public ProjectTimeFrame TimeFrame { get; private set; } + + public IReadOnlyList ProgressReports => _progressReports.AsReadOnly(); + public IReadOnlyList TeamMembers => _teamMembers.AsReadOnly(); + public IReadOnlyList Faq => _faq.AsReadOnly(); + public IReadOnlyList Categories => _categories.AsReadOnly(); + + private readonly List _progressReports = new(); + private readonly List _teamMembers = new(); + private readonly List _faq = new(); + private readonly List _categories = new(); + + public Project(string name, string description, ProjectTimeFrame timeFrame) + { + SetChangableValues(name, description, timeFrame); + } + + public void SetChangableValues(string name, string description, ProjectTimeFrame timeFrame) + { + name.ThrowIfEmptyOrNull(nameof(name)); + Name = name.Trim(); + + description.ThrowIfEmptyOrNull(nameof(description)); + Description = description.Trim(); + + TimeFrame = timeFrame; + } + + private Project() + { + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/TeamMember.cs b/backend/GameDevPortal.Core/Entities/TeamMember.cs new file mode 100644 index 0000000..d3ed414 --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/TeamMember.cs @@ -0,0 +1,18 @@ +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Entities; + +public class TeamMember : Entity +{ + public ProjectRole Role { get; private set; } + + public User? User { get; private set; } + public Guid UserId { get; private set; } + + public Project? Project { get; private set; } + public Guid ProjectId { get; private set; } + + private TeamMember() + { + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Entities/User.cs b/backend/GameDevPortal.Core/Entities/User.cs new file mode 100644 index 0000000..a74d223 --- /dev/null +++ b/backend/GameDevPortal.Core/Entities/User.cs @@ -0,0 +1,30 @@ +using GameDevPortal.Core.Extensions; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.ValueTypes; +using Microsoft.AspNetCore.Identity; +using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations.Schema; +using System.Xml.Linq; + +namespace GameDevPortal.Core.Entities; + +public class User : IdentityUser, IEntity +{ + public DateTime CreatedAt { get; private set; } + public DateTime UpdatedAt { get; private set; } + public bool IsDeleted { get; private set; } + public IReadOnlyList TeamMemberships => _teamMemberships.AsReadOnly(); + + private readonly List _teamMemberships = new List(); + + public void SetChangableValues(string username) + { + username.ThrowIfEmptyOrNull(nameof(username)); + UserName = username.Trim(); + } + + protected User() + { + IsDeleted = false; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Extensions/IEnumerableExtensions.cs b/backend/GameDevPortal.Core/Extensions/IEnumerableExtensions.cs new file mode 100644 index 0000000..ae3ec74 --- /dev/null +++ b/backend/GameDevPortal.Core/Extensions/IEnumerableExtensions.cs @@ -0,0 +1,30 @@ +using System.Text; +using System.Text.RegularExpressions; + +namespace GameDevPortal.Core.Extensions; + +public static class IEnumerableExtensions +{ + public static void ThrowIfEmptyOrNull(this IEnumerable enumerable, string parameterName) + { + if (enumerable == null) throw new ArgumentNullException($"IEnumerable parameter {parameterName} is null."); + if (!enumerable.Any()) throw new ArgumentException($"IEnumerable parameter {parameterName} is empty."); + } + + public static void ThrowIfStrictSuperset(this IEnumerable superset, IEnumerable subset, string itemName = "item") + { + if (subset.Count() != superset.Count()) + { + List missingItems = superset.Except(subset).ToList(); + + StringBuilder sb = new(); + sb.Append($"\"{missingItems[0]}\""); + for (int i = 1; i < missingItems.Count; i++) + { + sb.Append($", \"{missingItems[i]}\""); + } + + throw new KeyNotFoundException($"The following {itemName}s were not found: {sb}"); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Extensions/StringExtensions.cs b/backend/GameDevPortal.Core/Extensions/StringExtensions.cs new file mode 100644 index 0000000..4da9555 --- /dev/null +++ b/backend/GameDevPortal.Core/Extensions/StringExtensions.cs @@ -0,0 +1,44 @@ +using System.Linq.Expressions; +using System.Text; +using System; +using System.Text.RegularExpressions; +using static System.Runtime.InteropServices.JavaScript.JSType; + +namespace GameDevPortal.Core.Extensions; + +public static partial class StringExtensions +{ + public static void ThrowIfEmptyOrNull(this string str, string parameterName) + { + str = (str ?? string.Empty).Trim(); + if (str.Length == 0) throw new ArgumentException($"String parameter {parameterName} is empty."); + } + + public static bool IsEmptyContent(this string str) + { + return string.IsNullOrEmpty(str) || string.IsNullOrWhiteSpace(str); + } + + public static string FillTemplate(this string str, params Expression>[] args) + { + var parameters = args.ToDictionary(e => $"{{{e.Parameters[0].Name}}}", e => e.Compile()(e.Parameters[0].Name)); + + var sb = new StringBuilder(str); + foreach (var parameter in parameters) + { + sb.Replace(parameter.Key, parameter.Value != null ? parameter.Value.ToString() : string.Empty); + } + + return sb.ToString(); + } + + public static void ThrowIfNotHex(this string str, string parameterName) + { + Regex rx = HexRegex(); + + if (!rx.Match(str).Success) throw new ArgumentException($"Hex string {parameterName} is not a valid hexadecimal number."); + } + + [GeneratedRegex("#[A-F0-9]{6}")] + private static partial Regex HexRegex(); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/GameDevPortal.Core.csproj b/backend/GameDevPortal.Core/GameDevPortal.Core.csproj new file mode 100644 index 0000000..aec2d44 --- /dev/null +++ b/backend/GameDevPortal.Core/GameDevPortal.Core.csproj @@ -0,0 +1,16 @@ + + + + net7.0 + enable + enable + + + + + + + + + + diff --git a/backend/GameDevPortal.Core/IncludeLists/BaseIncludeList.cs b/backend/GameDevPortal.Core/IncludeLists/BaseIncludeList.cs new file mode 100644 index 0000000..9aa5d63 --- /dev/null +++ b/backend/GameDevPortal.Core/IncludeLists/BaseIncludeList.cs @@ -0,0 +1,23 @@ +using GameDevPortal.Core.Interfaces.Repositories; +using System.Linq.Expressions; + +namespace GameDevPortal.Core.IncludeLists; + +public class BaseIncludeList : IIncludeList +{ + public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + + public BaseIncludeList() { } + + protected virtual void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + + // string-based includes allow for including children of children, e.g. Basket.Items.Product + protected virtual void AddInclude(string includeString) + { + IncludeStrings.Add(includeString); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/IncludeLists/FullProgressReportIncludeList.cs b/backend/GameDevPortal.Core/IncludeLists/FullProgressReportIncludeList.cs new file mode 100644 index 0000000..7432727 --- /dev/null +++ b/backend/GameDevPortal.Core/IncludeLists/FullProgressReportIncludeList.cs @@ -0,0 +1,11 @@ +using GameDevPortal.Core.Entities; + +namespace GameDevPortal.Core.IncludeLists; + +public class FullProgressReportIncludeList : BaseIncludeList +{ + public FullProgressReportIncludeList() + { + AddInclude(pr => pr.Categories); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Authentication/IAuthenticationService.cs b/backend/GameDevPortal.Core/Interfaces/Authentication/IAuthenticationService.cs new file mode 100644 index 0000000..381bbaa --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Authentication/IAuthenticationService.cs @@ -0,0 +1,12 @@ + +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces.Authentication; + +public interface IAuthenticationService +{ + Task> ValidateUser(string username, string password); + Task> CreateToken(User user); + string CreateExpiredToken(); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Authentication/IUserService.cs b/backend/GameDevPortal.Core/Interfaces/Authentication/IUserService.cs new file mode 100644 index 0000000..6a83804 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Authentication/IUserService.cs @@ -0,0 +1,21 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Models; +using System.Runtime.CompilerServices; + +namespace GameDevPortal.Core.Interfaces.Authentication; + +public interface IUserService +{ + Task>> List(Pagination pagination, CancellationToken cancellationToken = default); + Task Create(User user, string password, IEnumerable roles); + Task> Get(Guid id); + Task Update(User user); + Task Delete(Guid id); + + Task> ValidatePassword(User user, string password); + Task>> GetRoles(User user); + Task> Find(string username); + Task Register(User user, string password); + Task SetRoles(User user, IEnumerable roles); + Task ChangePassword(User user, string currentPassword, string newPassword); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/DomainServices/ICategoryDomainService.cs b/backend/GameDevPortal.Core/Interfaces/DomainServices/ICategoryDomainService.cs new file mode 100644 index 0000000..48b3f57 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/DomainServices/ICategoryDomainService.cs @@ -0,0 +1,9 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.DomainServices; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces; + +public interface ICategoryDomainService : ICommonOperations +{ +} diff --git a/backend/GameDevPortal.Core/Interfaces/DomainServices/ICommonOperations.cs b/backend/GameDevPortal.Core/Interfaces/DomainServices/ICommonOperations.cs new file mode 100644 index 0000000..b692c46 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/DomainServices/ICommonOperations.cs @@ -0,0 +1,16 @@ +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces.DomainServices; + +public interface ICommonOperations +{ + public Task>> List(Pagination pagination, CancellationToken cancellationToken = default); + + public Task Insert(TEntity entity, CancellationToken cancellationToken = default); + + public Task> Get(Guid id, CancellationToken cancellationToken = default); + + public Task Update(TEntity entity, CancellationToken cancellationToken = default); + + public Task Delete(Guid id, CancellationToken cancellationToken = default); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/DomainServices/IFaqDomainService.cs b/backend/GameDevPortal.Core/Interfaces/DomainServices/IFaqDomainService.cs new file mode 100644 index 0000000..ca50a94 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/DomainServices/IFaqDomainService.cs @@ -0,0 +1,9 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.DomainServices; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces; + +public interface IFaqDomainService : ICommonOperations +{ +} diff --git a/backend/GameDevPortal.Core/Interfaces/DomainServices/IProgressReportDomainService.cs b/backend/GameDevPortal.Core/Interfaces/DomainServices/IProgressReportDomainService.cs new file mode 100644 index 0000000..29689f6 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/DomainServices/IProgressReportDomainService.cs @@ -0,0 +1,9 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.DomainServices; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces; + +public interface IProgressReportDomainService : ICommonOperations +{ +} diff --git a/backend/GameDevPortal.Core/Interfaces/DomainServices/IProjectDomainService.cs b/backend/GameDevPortal.Core/Interfaces/DomainServices/IProjectDomainService.cs new file mode 100644 index 0000000..9536e94 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/DomainServices/IProjectDomainService.cs @@ -0,0 +1,11 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.DomainServices; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Interfaces; + +public interface IProjectDomainService : ICommonOperations +{ + Task>> ListProgressReports(Guid id, Pagination pagination, CancellationToken cancellationToken = default); + Task>> ListCategories(Guid id, Pagination pagination, CancellationToken cancellationToken = default); +} diff --git a/backend/GameDevPortal.Core/Interfaces/IEntity.cs b/backend/GameDevPortal.Core/Interfaces/IEntity.cs new file mode 100644 index 0000000..6666afb --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/IEntity.cs @@ -0,0 +1,9 @@ +namespace GameDevPortal.Core.Interfaces; + +public interface IEntity +{ + Guid Id { get; } + DateTime CreatedAt { get; } + DateTime UpdatedAt { get; } + bool IsDeleted { get; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationBodyGenerator.cs b/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationBodyGenerator.cs new file mode 100644 index 0000000..588434c --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationBodyGenerator.cs @@ -0,0 +1,9 @@ +using GameDevPortal.Core.Models.Notifications; +using System.Linq.Expressions; + +namespace GameDevPortal.Core.Interfaces.Notifications; + +public interface INotificationBodyGenerator +{ + string Generate(ITemplateProvider templateProvider); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationService.cs b/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationService.cs new file mode 100644 index 0000000..e4d8e70 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Notifications/INotificationService.cs @@ -0,0 +1,9 @@ +using GameDevPortal.Core.Models; +using GameDevPortal.Core.Models.Notifications; + +namespace GameDevPortal.Core.Interfaces.Notifications; + +public interface INotificationService +{ + Task Send(Notification notification); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Notifications/ITemplateProvider.cs b/backend/GameDevPortal.Core/Interfaces/Notifications/ITemplateProvider.cs new file mode 100644 index 0000000..789f408 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Notifications/ITemplateProvider.cs @@ -0,0 +1,6 @@ +namespace GameDevPortal.Core.Interfaces.Notifications; + +public interface ITemplateProvider +{ + public abstract string ReadTemplate(string templateName); +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Repositories/IIncludeList.cs b/backend/GameDevPortal.Core/Interfaces/Repositories/IIncludeList.cs new file mode 100644 index 0000000..f28a8bd --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Repositories/IIncludeList.cs @@ -0,0 +1,9 @@ +using System.Linq.Expressions; + +namespace GameDevPortal.Core.Interfaces.Repositories; + +public interface IIncludeList +{ + List>> Includes { get; } + List IncludeStrings { get; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Repositories/IRepository.cs b/backend/GameDevPortal.Core/Interfaces/Repositories/IRepository.cs new file mode 100644 index 0000000..b374c98 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Repositories/IRepository.cs @@ -0,0 +1,20 @@ +using GameDevPortal.Core.Entities; + +namespace GameDevPortal.Core.Interfaces.Repositories; + +public interface IRepository +{ + public Task> List(CancellationToken cancellationToken = default) where T : Entity; + + public Task> List(ISpecification spec, CancellationToken cancellationToken = default) where T : Entity; + + public Task Insert(T entity, CancellationToken cancellationToken = default) where T : Entity; + + public Task Get(Guid id, CancellationToken cancellationToken = default) where T : Entity; + + public Task Get(Guid id, IIncludeList includeList, CancellationToken cancellationToken = default) where T : Entity; + + public Task Update(T entity, CancellationToken cancellationToken = default) where T : Entity; + + public Task Delete(Guid id, CancellationToken cancellationToken = default) where T : Entity; +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Interfaces/Repositories/ISpecification.cs b/backend/GameDevPortal.Core/Interfaces/Repositories/ISpecification.cs new file mode 100644 index 0000000..87a5002 --- /dev/null +++ b/backend/GameDevPortal.Core/Interfaces/Repositories/ISpecification.cs @@ -0,0 +1,13 @@ +using System.Linq.Expressions; + +namespace GameDevPortal.Core.Interfaces.Repositories; + +public interface ISpecification +{ + int PageSize { get; } + int PageIndex { get; } + + Expression> Criteria { get; } + List>> Includes { get; } + List IncludeStrings { get; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/Notifications/Notification.cs b/backend/GameDevPortal.Core/Models/Notifications/Notification.cs new file mode 100644 index 0000000..5f484a7 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/Notifications/Notification.cs @@ -0,0 +1,24 @@ +using GameDevPortal.Core.Extensions; + +namespace GameDevPortal.Core.Models.Notifications; + +public class Notification +{ + public string Title { get; private set; } + public string Body { get; private set; } + public IReadOnlyList Recipients => _recipients.AsReadOnly(); + + private readonly List _recipients = new(); + + public Notification(string title, string body, IEnumerable recipients) + { + title.ThrowIfEmptyOrNull(nameof(title)); + Title = title; + + body.ThrowIfEmptyOrNull(nameof(body)); + Body = body; + + recipients.ThrowIfEmptyOrNull(nameof(recipients)); + _recipients.AddRange(recipients); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/Notifications/NotificationData/BasicNotificationModel.cs b/backend/GameDevPortal.Core/Models/Notifications/NotificationData/BasicNotificationModel.cs new file mode 100644 index 0000000..2ef0291 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/Notifications/NotificationData/BasicNotificationModel.cs @@ -0,0 +1,30 @@ +using GameDevPortal.Core.Extensions; +using GameDevPortal.Core.Interfaces.Notifications; +using System.Reflection; +using static System.Runtime.InteropServices.JavaScript.JSType; +using System.Text; + +namespace GameDevPortal.Core.Models.Notifications.NotificationData; + +public class BasicNotificationModel : INotificationBodyGenerator +{ + private const string _templateFile = "basicTemplate.txt"; + public string ProjectTitle { get; private set; } + public string ProjectDescription { get; private set; } + + public BasicNotificationModel(string projectTitle, string projectDescription) + { + projectTitle.ThrowIfEmptyOrNull(nameof(projectTitle)); + ProjectTitle = projectTitle; + + projectTitle.ThrowIfEmptyOrNull(nameof(projectTitle)); + ProjectDescription = projectDescription; + } + + public string Generate(ITemplateProvider templateProvider) + { + string template = templateProvider.ReadTemplate(_templateFile); + + return template.FillTemplate(Title => ProjectTitle, Description => ProjectDescription); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/OperationResult.cs b/backend/GameDevPortal.Core/Models/OperationResult.cs new file mode 100644 index 0000000..48af3a2 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/OperationResult.cs @@ -0,0 +1,46 @@ +namespace GameDevPortal.Core.Models; + +public class OperationResult +{ + public bool Success { get; protected set; } + public string ErrorMessage { get; protected set; } + public Exception Exception { get; protected set; } + + public static OperationResult CreateSuccessResult() + { + return new OperationResult { Success = true }; + } + + public static OperationResult CreateFailure(Exception ex) + { + return new OperationResult + { + Success = false, + ErrorMessage = ex.Message, + Exception = ex + }; + } + +} + +public class OperationResult : OperationResult +{ + private OperationResult() { } + + public TResult ResultData { get; private set; } + + public static OperationResult CreateSuccessResult(TResult result) + { + return new OperationResult { Success = true, ResultData = result }; + } + + public static new OperationResult CreateFailure(Exception ex) + { + return new OperationResult + { + Success = false, + ErrorMessage = ex.Message, + Exception = ex + }; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/Pagination.cs b/backend/GameDevPortal.Core/Models/Pagination.cs new file mode 100644 index 0000000..bb2796b --- /dev/null +++ b/backend/GameDevPortal.Core/Models/Pagination.cs @@ -0,0 +1,42 @@ +namespace GameDevPortal.Core.Models; + +public record Pagination +{ + private const int _maxPageSize = 20; + private const int _defaultSize = 10; + + public int Size + { + get + { + return _size; + } + set + { + _size = Math.Max(Math.Min(value, _maxPageSize), 1); + } + } + + public int Index + { + get + { + return _index; + } + set + { + _index = Math.Max(value, 0); + } + } + + private int _size = _defaultSize; + private int _index = 0; + + public Pagination(int size, int index) + { + Size = size; + Index = index; + } + + public Pagination() { } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/ProjectRole.cs b/backend/GameDevPortal.Core/Models/ProjectRole.cs new file mode 100644 index 0000000..7c92bea --- /dev/null +++ b/backend/GameDevPortal.Core/Models/ProjectRole.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace GameDevPortal.Core.Models; + +public enum ProjectRole +{ + None, + Contributor, + Moderator, + Admin, + Owner +} diff --git a/backend/GameDevPortal.Core/Models/ProjectStatus.cs b/backend/GameDevPortal.Core/Models/ProjectStatus.cs new file mode 100644 index 0000000..67d12b7 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/ProjectStatus.cs @@ -0,0 +1,10 @@ +namespace GameDevPortal.Core.Models; + +public enum ProjectStatus +{ + Concept, + Preparation, + Production, + Testing, + Finished +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/TokenDto.cs b/backend/GameDevPortal.Core/Models/TokenDto.cs new file mode 100644 index 0000000..3d9b055 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/TokenDto.cs @@ -0,0 +1,14 @@ +using GameDevPortal.Core.Extensions; + +namespace GameDevPortal.Core.Models; + +public class TokenDto +{ + public string AccessToken { get; set; } + + public TokenDto(string accessToken) + { + accessToken.ThrowIfEmptyOrNull(nameof(accessToken)); + AccessToken = accessToken; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Models/UserRole.cs b/backend/GameDevPortal.Core/Models/UserRole.cs new file mode 100644 index 0000000..5f5c4f0 --- /dev/null +++ b/backend/GameDevPortal.Core/Models/UserRole.cs @@ -0,0 +1,7 @@ +using Microsoft.AspNetCore.Identity; + +namespace GameDevPortal.Core.Models; + +public class UserRole : IdentityRole +{ +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Services/DomainServices/CategoryDomainService.cs b/backend/GameDevPortal.Core/Services/DomainServices/CategoryDomainService.cs new file mode 100644 index 0000000..e3057fa --- /dev/null +++ b/backend/GameDevPortal.Core/Services/DomainServices/CategoryDomainService.cs @@ -0,0 +1,91 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Models; +using GameDevPortal.Core.Specifications; +using Microsoft.Extensions.Logging; + +namespace GameDevPortal.Core.Services.DomainServices; + +public class CategoryDomainService : ICategoryDomainService +{ + private readonly IRepository _repository; + private readonly ILogger _logger; + + public CategoryDomainService(IRepository repository, ILogger logger) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task>> List(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new BaseSpecification(pagination); + IEnumerable categories = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(categories); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } + + public async Task Insert(Category category, CancellationToken cancellationToken = default) + { + try + { + await _repository.Insert(category, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + try + { + Category result = await _repository.Get(id, cancellationToken); + return OperationResult.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Update(Category updateCategory, CancellationToken cancellationToken = default) + { + try + { + await _repository.Update(updateCategory, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + try + { + await _repository.Delete(id, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Services/DomainServices/FaqDomainService.cs b/backend/GameDevPortal.Core/Services/DomainServices/FaqDomainService.cs new file mode 100644 index 0000000..76d4745 --- /dev/null +++ b/backend/GameDevPortal.Core/Services/DomainServices/FaqDomainService.cs @@ -0,0 +1,91 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Models; +using GameDevPortal.Core.Specifications; +using Microsoft.Extensions.Logging; + +namespace GameDevPortal.Core.Services.DomainServices; + +public class FaqDomainService : IFaqDomainService +{ + private readonly IRepository _repository; + private readonly ILogger _logger; + + public FaqDomainService(IRepository repository, ILogger logger) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task>> List(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new BaseSpecification(pagination); + IEnumerable categories = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(categories); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } + + public async Task Insert(Faq faq, CancellationToken cancellationToken = default) + { + try + { + await _repository.Insert(faq, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + try + { + Faq result = await _repository.Get(id, cancellationToken); + return OperationResult.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Update(Faq updateFaq, CancellationToken cancellationToken = default) + { + try + { + await _repository.Update(updateFaq, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + try + { + await _repository.Delete(id, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Services/DomainServices/ProgressReportDomainService.cs b/backend/GameDevPortal.Core/Services/DomainServices/ProgressReportDomainService.cs new file mode 100644 index 0000000..b22ffba --- /dev/null +++ b/backend/GameDevPortal.Core/Services/DomainServices/ProgressReportDomainService.cs @@ -0,0 +1,113 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Extensions; +using GameDevPortal.Core.IncludeLists; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Models; +using GameDevPortal.Core.Specifications; +using Microsoft.Extensions.Logging; +using System.Text; + +namespace GameDevPortal.Core.Services.DomainServices; + +public class ProgressReportDomainService : IProgressReportDomainService +{ + private readonly IRepository _repository; + private readonly ILogger _logger; + + public ProgressReportDomainService(IRepository repository, ILogger logger) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task>> List(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new FullProgressReportSpecification(pagination); + IEnumerable progressReports = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(progressReports); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } + + public async Task Insert(ProgressReport progressReport, CancellationToken cancellationToken = default) + { + try + { + BaseSpecification specification = new CategoriesForProjectInSet(progressReport.ProjectId, progressReport.CategoryIds.ToHashSet()); + IEnumerable categories = await _repository.List(specification, cancellationToken); + + progressReport.CategoryIds.ThrowIfStrictSuperset(categories.Select(c => c.Id), "category ID"); + + progressReport.AddCategories(categories); + + await _repository.Insert(progressReport, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + try + { + FullProgressReportIncludeList includeList = new(); + ProgressReport result = await _repository.Get(id, includeList, cancellationToken); + result.SyncCategoryIdsToCategories(); + + return OperationResult.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Update(ProgressReport updateProgressReport, CancellationToken cancellationToken = default) + { + try + { + updateProgressReport.ClearCategories(); + + BaseSpecification specification = new CategoriesForProjectInSet(updateProgressReport.ProjectId, updateProgressReport.CategoryIds.ToHashSet()); + IEnumerable categories = await _repository.List(specification, cancellationToken); + + updateProgressReport.CategoryIds.ThrowIfStrictSuperset(categories.Select(c => c.Id), "category ID"); + + updateProgressReport.AddCategories(categories); + + await _repository.Update(updateProgressReport, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + try + { + await _repository.Delete(id, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Services/DomainServices/ProjectDomainService.cs b/backend/GameDevPortal.Core/Services/DomainServices/ProjectDomainService.cs new file mode 100644 index 0000000..b83822a --- /dev/null +++ b/backend/GameDevPortal.Core/Services/DomainServices/ProjectDomainService.cs @@ -0,0 +1,124 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Interfaces.Notifications; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Models; +using GameDevPortal.Core.Models.Notifications; +using GameDevPortal.Core.Models.Notifications.NotificationData; +using GameDevPortal.Core.Specifications; +using Microsoft.Extensions.Logging; + +namespace GameDevPortal.Core.Services.DomainServices; + +public class ProjectDomainService : IProjectDomainService +{ + private readonly IRepository _repository; + private readonly ILogger _logger; + + public ProjectDomainService(IRepository repository, ILogger logger, ITemplateProvider templateProvider) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task>> List(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new BaseSpecification(pagination); + IEnumerable projects = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(projects); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } + + public async Task Insert(Project project, CancellationToken cancellationToken = default) + { + try + { + await _repository.Insert(project, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + try + { + Project result = await _repository.Get(id, cancellationToken); + return OperationResult.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Update(Project updateProject, CancellationToken cancellationToken = default) + { + try + { + await _repository.Update(updateProject, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + try + { + await _repository.Delete(id, cancellationToken); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult.CreateFailure(ex); + } + } + + public async Task>> ListProgressReports(Guid id, Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new FullProgressReportSpecification(pr => pr.ProjectId == id, pagination); + IEnumerable result = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } + + public async Task>> ListCategories(Guid id, Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + ISpecification specification = new BaseSpecification(c => c.ProjectId == null || c.ProjectId == id, pagination); + IEnumerable result = await _repository.List(specification, cancellationToken); + return OperationResult>.CreateSuccessResult(result); + } + catch (Exception ex) + { + _logger.LogError(ex.ToString()); + return OperationResult>.CreateFailure(ex); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Specifications/BaseSpecification.cs b/backend/GameDevPortal.Core/Specifications/BaseSpecification.cs new file mode 100644 index 0000000..1bf04f0 --- /dev/null +++ b/backend/GameDevPortal.Core/Specifications/BaseSpecification.cs @@ -0,0 +1,47 @@ +using System.Linq.Expressions; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.Specifications; + +public class BaseSpecification : ISpecification +{ + public int PageSize { get; private set; } + public int PageIndex { get; private set; } + + public BaseSpecification(Expression> criteria, Pagination pagination) + { + Criteria = criteria; + PageSize = pagination.Size; + PageIndex = pagination.Index; + } + + public BaseSpecification(Expression> criteria) + { + Criteria = criteria; + PageSize = int.MaxValue; + PageIndex = 0; + } + + public BaseSpecification(Pagination pagination) + { + Criteria = _ => true; + PageSize = pagination.Size; + PageIndex = pagination.Index; + } + + public Expression> Criteria { get; } + public List>> Includes { get; } = new List>>(); + public List IncludeStrings { get; } = new List(); + + protected virtual void AddInclude(Expression> includeExpression) + { + Includes.Add(includeExpression); + } + + // string-based includes allow for including children of children, e.g. Basket.Items.Product + protected virtual void AddInclude(string includeString) + { + IncludeStrings.Add(includeString); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/Specifications/CategoriesForProjectInSet.cs b/backend/GameDevPortal.Core/Specifications/CategoriesForProjectInSet.cs new file mode 100644 index 0000000..79287b7 --- /dev/null +++ b/backend/GameDevPortal.Core/Specifications/CategoriesForProjectInSet.cs @@ -0,0 +1,18 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Models; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace GameDevPortal.Core.Specifications +{ + internal class CategoriesForProjectInSet : BaseSpecification + { + public CategoriesForProjectInSet(Guid projectId, HashSet categoryIds) : base(c => (c.ProjectId == null || c.ProjectId == projectId) && categoryIds.Contains(c.Id)) + { + } + } +} diff --git a/backend/GameDevPortal.Core/Specifications/FullProgressReportSpecification.cs b/backend/GameDevPortal.Core/Specifications/FullProgressReportSpecification.cs new file mode 100644 index 0000000..5f0b76a --- /dev/null +++ b/backend/GameDevPortal.Core/Specifications/FullProgressReportSpecification.cs @@ -0,0 +1,19 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Models; +using System.Linq.Expressions; + +namespace GameDevPortal.Core.Specifications +{ + internal class FullProgressReportSpecification : BaseSpecification + { + public FullProgressReportSpecification(Pagination pagination) : base(pagination) + { + AddInclude(pr => pr.Categories); + } + + public FullProgressReportSpecification(Expression> criteria, Pagination pagination) : base(criteria, pagination) + { + AddInclude(pr => pr.Categories); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Core/ValueTypes/ProjectTimeframe.cs b/backend/GameDevPortal.Core/ValueTypes/ProjectTimeframe.cs new file mode 100644 index 0000000..36b4b2a --- /dev/null +++ b/backend/GameDevPortal.Core/ValueTypes/ProjectTimeframe.cs @@ -0,0 +1,21 @@ +using GameDevPortal.Core.Models; + +namespace GameDevPortal.Core.ValueTypes; + +public class ProjectTimeFrame +{ + public DateTime StartDate { get; private set; } + public DateTime EndDate { get; private set; } + public ProjectStatus Status { get; private set; } + + public ProjectTimeFrame(DateTime startDate, DateTime endDate, ProjectStatus status) + { + if (startDate >= endDate) throw new ArgumentException("Project start date must be before the end date.", nameof(startDate)); + StartDate = startDate; + EndDate = endDate; + + if (status == ProjectStatus.Finished && endDate > DateTime.UtcNow) throw new ArgumentException("Project status can not be finished if the end date of the project has not passed.", nameof(status)); + if (status > ProjectStatus.Preparation && startDate > DateTime.UtcNow) throw new ArgumentException("Projects which have entered production must have a start date before the current date.", nameof(status)); + Status = status; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/Data/Configuration/ProjectEntityTypeConfiguration.cs b/backend/GameDevPortal.Infrastructure/Data/Configuration/ProjectEntityTypeConfiguration.cs new file mode 100644 index 0000000..bb47ca0 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Configuration/ProjectEntityTypeConfiguration.cs @@ -0,0 +1,14 @@ +using GameDevPortal.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.Reflection.Metadata; + +namespace GameDevPortal.Infrastructure.Data.Configuration; + +public class ProjectEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.OwnsOne(p => p.TimeFrame); + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Configuration/TeamMemberEntityTypeConfiguration.cs b/backend/GameDevPortal.Infrastructure/Data/Configuration/TeamMemberEntityTypeConfiguration.cs new file mode 100644 index 0000000..9f71471 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Configuration/TeamMemberEntityTypeConfiguration.cs @@ -0,0 +1,18 @@ +using GameDevPortal.Core.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.Reflection.Emit; +using System.Reflection.Metadata; + +namespace GameDevPortal.Infrastructure.Data.Configuration; + +public class TeamMemberEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasKey(tm => new { tm.UserId, tm.ProjectId }); + + builder.HasOne(tm => tm.User).WithMany(u => u.TeamMemberships).HasForeignKey(tm => tm.UserId); + builder.HasOne(tm => tm.Project).WithMany(p => p.TeamMembers).HasForeignKey(tm => tm.ProjectId); + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Configuration/UserEntityTypeConfiguration.cs b/backend/GameDevPortal.Infrastructure/Data/Configuration/UserEntityTypeConfiguration.cs new file mode 100644 index 0000000..991f54b --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Configuration/UserEntityTypeConfiguration.cs @@ -0,0 +1,17 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using System.Reflection.Emit; +using System.Reflection.Metadata; + +namespace GameDevPortal.Infrastructure.Data.Configuration; + +public class UserEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.Property(u => u.Id).HasColumnType("uniqueidentifier"); + builder.HasKey(u => u.Id); + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Configuration/UserRoleEntityTypeConfiguration.cs b/backend/GameDevPortal.Infrastructure/Data/Configuration/UserRoleEntityTypeConfiguration.cs new file mode 100644 index 0000000..84a0a78 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Configuration/UserRoleEntityTypeConfiguration.cs @@ -0,0 +1,33 @@ +using GameDevPortal.Core.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace GameDevPortal.Infrastructure.Data.Configuration; + +public class UserRoleEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.HasData(new List + { + new UserRole() + { + Id = new Guid("662ce9ac-052d-4211-ae0d-50883ccdf872"), + Name = "Administrator", + NormalizedName = "ADMINISTRATOR" + }, + new UserRole() + { + Id = new Guid("094bf682-2c34-4d18-a24f-a20ffbd232be"), + Name = "Moderator", + NormalizedName = "MODERATOR" + }, + new UserRole() + { + Id = new Guid("27a71320-dfd4-4833-bbad-5f7fc5670532"), + Name = "User", + NormalizedName = "USER" + } + }); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/Data/EfCoreRepository.cs b/backend/GameDevPortal.Infrastructure/Data/EfCoreRepository.cs new file mode 100644 index 0000000..6be1a55 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/EfCoreRepository.cs @@ -0,0 +1,68 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.Repositories; +using Microsoft.EntityFrameworkCore; + +namespace GameDevPortal.Infrastructure.Data; + +public class EfCoreRepository : IRepository +{ + private readonly ProjectContext _context; + + public EfCoreRepository(ProjectContext context) + { + _context = context; + } + + public async Task> List(CancellationToken cancellationToken = default) where T : Entity + { + return await _context.Set().OrderBy(t => t.Id).ToListAsync(cancellationToken); + } + + // https://github.com/dotnet-architecture/eShopOnWeb + public async Task> List(ISpecification spec, CancellationToken cancellationToken = default) where T : Entity + { + // fetch a Queryable that includes all expression-based includes + var queryableResultWithIncludes = spec.Includes.Aggregate(_context.Set().AsQueryable(), (current, include) => current.Include(include)); + + // modify the IQueryable to include any string-based include statements + var secondaryResult = spec.IncludeStrings.Aggregate(queryableResultWithIncludes, (current, include) => current.Include(include)); + + // return the result of the query using the specification's criteria expression + return await secondaryResult.Where(spec.Criteria).OrderBy(t => t.Id).Skip(spec.PageSize * spec.PageIndex).Take(spec.PageSize).ToListAsync(cancellationToken); + } + + public async Task Insert(T entity, CancellationToken cancellationToken = default) where T : Entity + { + await _context.Set().AddAsync(entity, cancellationToken); + + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task Get(Guid id, CancellationToken cancellationToken = default) where T : Entity + { + return await _context.Set().FindAsync(id, cancellationToken) ?? throw new KeyNotFoundException(id.ToString()); + } + + public async Task Get(Guid id, IIncludeList includeList, CancellationToken cancellationToken = default) where T : Entity + { + var queryableResultWithIncludes = includeList.Includes.Aggregate(_context.Set().AsQueryable(), (current, include) => current.Include(include)); + + // modify the IQueryable to include any string-based include statements + var secondaryResult = includeList.IncludeStrings.Aggregate(queryableResultWithIncludes, (current, include) => current.Include(include)); + + // return the result of the query using the specification's criteria expression + return await secondaryResult.SingleAsync(t => t.Id == id, cancellationToken) ?? throw new KeyNotFoundException(id.ToString()); + } + + public async Task Update(T entity, CancellationToken cancellationToken = default) where T : Entity + { + _context.Set().Update(entity); + await _context.SaveChangesAsync(cancellationToken); + } + + public async Task Delete(Guid id, CancellationToken cancellationToken = default) where T : Entity + { + _context.Set().Remove(await _context.Set().FindAsync(id, cancellationToken) ?? throw new KeyNotFoundException()); + await _context.SaveChangesAsync(cancellationToken); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.Designer.cs new file mode 100644 index 0000000..c75e9cb --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.Designer.cs @@ -0,0 +1,203 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230224130939_Initial")] + partial class Initial + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("FAQ"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("FAQ") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("ProgressReports") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("FAQ"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.cs new file mode 100644 index 0000000..46b49a5 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224130939_Initial.cs @@ -0,0 +1,127 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class Initial : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "Projects", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(100)", maxLength: 100, nullable: false), + Description = table.Column(type: "nvarchar(500)", maxLength: 500, nullable: false), + TimeFrame_StartDate = table.Column(type: "datetime2", nullable: false), + TimeFrame_EndDate = table.Column(type: "datetime2", nullable: false), + TimeFrame_Status = table.Column(type: "int", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Projects", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "FAQ", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Question = table.Column(type: "nvarchar(150)", maxLength: 150, nullable: false), + Answer = table.Column(type: "nvarchar(1500)", maxLength: 1500, nullable: false), + ProjectId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FAQ", x => x.Id); + table.ForeignKey( + name: "FK_FAQ_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "ProgressReport", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ProjectId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ProgressReport", x => x.Id); + table.ForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id"); + }); + + migrationBuilder.CreateTable( + name: "TeamMember", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ProjectId = table.Column(type: "uniqueidentifier", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_TeamMember", x => x.Id); + table.ForeignKey( + name: "FK_TeamMember_Projects_ProjectId", + column: x => x.ProjectId, + principalTable: "Projects", + principalColumn: "Id"); + }); + + migrationBuilder.CreateIndex( + name: "IX_FAQ_ProjectId", + table: "FAQ", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_ProgressReport_ProjectId", + table: "ProgressReport", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_TeamMember_ProjectId", + table: "TeamMember", + column: "ProjectId"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "FAQ"); + + migrationBuilder.DropTable( + name: "ProgressReport"); + + migrationBuilder.DropTable( + name: "TeamMember"); + + migrationBuilder.DropTable( + name: "Projects"); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.Designer.cs new file mode 100644 index 0000000..04cfaf6 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230224135401_ProgressReportAndNavigation")] + partial class ProgressReportAndNavigation + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgressReportId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProgressReportId"); + + b.ToTable("Category"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("FAQ"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PostedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", "ProgressReport") + .WithMany("Categories") + .HasForeignKey("ProgressReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("FAQ") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("FAQ"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.cs new file mode 100644 index 0000000..b6acb93 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224135401_ProgressReportAndNavigation.cs @@ -0,0 +1,167 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class ProgressReportAndNavigation : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ"); + + migrationBuilder.DropForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport"); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "ProgressReport", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "Description", + table: "ProgressReport", + type: "nvarchar(500)", + maxLength: 500, + nullable: false, + defaultValue: ""); + + migrationBuilder.AddColumn( + name: "PostedAt", + table: "ProgressReport", + type: "datetime2", + nullable: false, + defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified)); + + migrationBuilder.AddColumn( + name: "Title", + table: "ProgressReport", + type: "nvarchar(150)", + maxLength: 150, + nullable: false, + defaultValue: ""); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "FAQ", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.CreateTable( + name: "Category", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(50)", maxLength: 50, nullable: false), + HexColour = table.Column(type: "nvarchar(7)", maxLength: 7, nullable: false), + ProgressReportId = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_Category", x => x.Id); + table.ForeignKey( + name: "FK_Category_ProgressReport_ProgressReportId", + column: x => x.ProgressReportId, + principalTable: "ProgressReport", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Category_ProgressReportId", + table: "Category", + column: "ProgressReportId"); + + migrationBuilder.AddForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ"); + + migrationBuilder.DropForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport"); + + migrationBuilder.DropTable( + name: "Category"); + + migrationBuilder.DropColumn( + name: "Description", + table: "ProgressReport"); + + migrationBuilder.DropColumn( + name: "PostedAt", + table: "ProgressReport"); + + migrationBuilder.DropColumn( + name: "Title", + table: "ProgressReport"); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "ProgressReport", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "FAQ", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id"); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.Designer.cs new file mode 100644 index 0000000..2489559 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230224162606_ProgressReportTable")] + partial class ProgressReportTable + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgressReportId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProgressReportId"); + + b.ToTable("Category"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("FAQ"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("PostedAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", "ProgressReport") + .WithMany("Categories") + .HasForeignKey("ProgressReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("FAQ") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("FAQ"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.cs new file mode 100644 index 0000000..806ed7c --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230224162606_ProgressReportTable.cs @@ -0,0 +1,102 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class ProgressReportTable : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Category_ProgressReport_ProgressReportId", + table: "Category"); + + migrationBuilder.DropForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ProgressReport", + table: "ProgressReport"); + + migrationBuilder.RenameTable( + name: "ProgressReport", + newName: "ProgressReports"); + + migrationBuilder.RenameIndex( + name: "IX_ProgressReport_ProjectId", + table: "ProgressReports", + newName: "IX_ProgressReports_ProjectId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ProgressReports", + table: "ProgressReports", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Category_ProgressReports_ProgressReportId", + table: "Category", + column: "ProgressReportId", + principalTable: "ProgressReports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ProgressReports_Projects_ProjectId", + table: "ProgressReports", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Category_ProgressReports_ProgressReportId", + table: "Category"); + + migrationBuilder.DropForeignKey( + name: "FK_ProgressReports_Projects_ProjectId", + table: "ProgressReports"); + + migrationBuilder.DropPrimaryKey( + name: "PK_ProgressReports", + table: "ProgressReports"); + + migrationBuilder.RenameTable( + name: "ProgressReports", + newName: "ProgressReport"); + + migrationBuilder.RenameIndex( + name: "IX_ProgressReports_ProjectId", + table: "ProgressReport", + newName: "IX_ProgressReport_ProjectId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_ProgressReport", + table: "ProgressReport", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Category_ProgressReport_ProgressReportId", + table: "Category", + column: "ProgressReportId", + principalTable: "ProgressReport", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_ProgressReport_Projects_ProjectId", + table: "ProgressReport", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.Designer.cs new file mode 100644 index 0000000..af46ace --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.Designer.cs @@ -0,0 +1,275 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230227102236_RenamePostedAt")] + partial class RenamePostedAt + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProgressReportId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProgressReportId"); + + b.ToTable("Category"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("FAQ"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", "ProgressReport") + .WithMany("Categories") + .HasForeignKey("ProgressReportId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("FAQ") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Navigation("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("FAQ"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.cs new file mode 100644 index 0000000..4b3c429 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227102236_RenamePostedAt.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class RenamePostedAt : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "PostedAt", + table: "ProgressReports", + newName: "MadePublicAt"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.RenameColumn( + name: "MadePublicAt", + table: "ProgressReports", + newName: "PostedAt"); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.Designer.cs new file mode 100644 index 0000000..443aed6 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.Designer.cs @@ -0,0 +1,298 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230227143800_CategoryFleshedOut")] + partial class CategoryFleshedOut + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("FAQ"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.FAQ", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("FAQ") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("FAQ"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.cs new file mode 100644 index 0000000..dc07180 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230227143800_CategoryFleshedOut.cs @@ -0,0 +1,139 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class CategoryFleshedOut : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Category_ProgressReports_ProgressReportId", + table: "Category"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Category", + table: "Category"); + + migrationBuilder.DropIndex( + name: "IX_Category_ProgressReportId", + table: "Category"); + + migrationBuilder.DropColumn( + name: "ProgressReportId", + table: "Category"); + + migrationBuilder.RenameTable( + name: "Category", + newName: "Categories"); + + migrationBuilder.AddColumn( + name: "ProjectId", + table: "Categories", + type: "uniqueidentifier", + nullable: true); + + migrationBuilder.AddPrimaryKey( + name: "PK_Categories", + table: "Categories", + column: "Id"); + + migrationBuilder.CreateTable( + name: "CategoryProgressReport", + columns: table => new + { + CategoriesId = table.Column(type: "uniqueidentifier", nullable: false), + ProgressReportsId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_CategoryProgressReport", x => new { x.CategoriesId, x.ProgressReportsId }); + table.ForeignKey( + name: "FK_CategoryProgressReport_Categories_CategoriesId", + column: x => x.CategoriesId, + principalTable: "Categories", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_CategoryProgressReport_ProgressReports_ProgressReportsId", + column: x => x.ProgressReportsId, + principalTable: "ProgressReports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_Categories_ProjectId", + table: "Categories", + column: "ProjectId"); + + migrationBuilder.CreateIndex( + name: "IX_CategoryProgressReport_ProgressReportsId", + table: "CategoryProgressReport", + column: "ProgressReportsId"); + + migrationBuilder.AddForeignKey( + name: "FK_Categories_Projects_ProjectId", + table: "Categories", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Categories_Projects_ProjectId", + table: "Categories"); + + migrationBuilder.DropTable( + name: "CategoryProgressReport"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Categories", + table: "Categories"); + + migrationBuilder.DropIndex( + name: "IX_Categories_ProjectId", + table: "Categories"); + + migrationBuilder.DropColumn( + name: "ProjectId", + table: "Categories"); + + migrationBuilder.RenameTable( + name: "Categories", + newName: "Category"); + + migrationBuilder.AddColumn( + name: "ProgressReportId", + table: "Category", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddPrimaryKey( + name: "PK_Category", + table: "Category", + column: "Id"); + + migrationBuilder.CreateIndex( + name: "IX_Category_ProgressReportId", + table: "Category", + column: "ProgressReportId"); + + migrationBuilder.AddForeignKey( + name: "FK_Category_ProgressReports_ProgressReportId", + table: "Category", + column: "ProgressReportId", + principalTable: "ProgressReports", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.Designer.cs new file mode 100644 index 0000000..ab6b0e1 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.Designer.cs @@ -0,0 +1,298 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230329124944_RenameFaq")] + partial class RenameFaq + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.3") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Faqs"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMember"); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("Faq") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("TeamMembers") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("Faq"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.cs new file mode 100644 index 0000000..93dbccc --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230329124944_RenameFaq.cs @@ -0,0 +1,78 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class RenameFaq : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ"); + + migrationBuilder.DropPrimaryKey( + name: "PK_FAQ", + table: "FAQ"); + + migrationBuilder.RenameTable( + name: "FAQ", + newName: "Faqs"); + + migrationBuilder.RenameIndex( + name: "IX_FAQ_ProjectId", + table: "Faqs", + newName: "IX_Faqs_ProjectId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_Faqs", + table: "Faqs", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_Faqs_Projects_ProjectId", + table: "Faqs", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_Faqs_Projects_ProjectId", + table: "Faqs"); + + migrationBuilder.DropPrimaryKey( + name: "PK_Faqs", + table: "Faqs"); + + migrationBuilder.RenameTable( + name: "Faqs", + newName: "FAQ"); + + migrationBuilder.RenameIndex( + name: "IX_Faqs_ProjectId", + table: "FAQ", + newName: "IX_FAQ_ProjectId"); + + migrationBuilder.AddPrimaryKey( + name: "PK_FAQ", + table: "FAQ", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_FAQ_Projects_ProjectId", + table: "FAQ", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.Designer.cs new file mode 100644 index 0000000..9c351ba --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.Designer.cs @@ -0,0 +1,577 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230406095708_AddUsers")] + partial class AddUsers + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Faqs"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "ProjectId"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("GameDevPortal.Core.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("Faq") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("TeamMembers") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", "User") + .WithMany("TeamMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("Faq"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Navigation("TeamMemberships"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.cs new file mode 100644 index 0000000..34f19cb --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406095708_AddUsers.cs @@ -0,0 +1,338 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class AddUsers : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TeamMember_Projects_ProjectId", + table: "TeamMember"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TeamMember", + table: "TeamMember"); + + migrationBuilder.RenameTable( + name: "TeamMember", + newName: "TeamMembers"); + + migrationBuilder.RenameIndex( + name: "IX_TeamMember_ProjectId", + table: "TeamMembers", + newName: "IX_TeamMembers_ProjectId"); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "TeamMembers", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000"), + oldClrType: typeof(Guid), + oldType: "uniqueidentifier", + oldNullable: true); + + migrationBuilder.AddColumn( + name: "UserId", + table: "TeamMembers", + type: "uniqueidentifier", + nullable: false, + defaultValue: new Guid("00000000-0000-0000-0000-000000000000")); + + migrationBuilder.AddColumn( + name: "Role", + table: "TeamMembers", + type: "int", + nullable: false, + defaultValue: 0); + + migrationBuilder.AddPrimaryKey( + name: "PK_TeamMembers", + table: "TeamMembers", + columns: new[] { "UserId", "ProjectId" }); + + migrationBuilder.CreateTable( + name: "AspNetRoles", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + Name = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoles", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetUsers", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: false), + IsDeleted = table.Column(type: "bit", nullable: false), + UserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedUserName = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + Email = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + NormalizedEmail = table.Column(type: "nvarchar(256)", maxLength: 256, nullable: true), + EmailConfirmed = table.Column(type: "bit", nullable: false), + PasswordHash = table.Column(type: "nvarchar(max)", nullable: true), + SecurityStamp = table.Column(type: "nvarchar(max)", nullable: true), + ConcurrencyStamp = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumber = table.Column(type: "nvarchar(max)", nullable: true), + PhoneNumberConfirmed = table.Column(type: "bit", nullable: false), + TwoFactorEnabled = table.Column(type: "bit", nullable: false), + LockoutEnd = table.Column(type: "datetimeoffset", nullable: true), + LockoutEnabled = table.Column(type: "bit", nullable: false), + AccessFailedCount = table.Column(type: "int", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUsers", x => x.Id); + }); + + migrationBuilder.CreateTable( + name: "AspNetRoleClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + RoleId = table.Column(type: "uniqueidentifier", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetRoleClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetRoleClaims_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserClaims", + columns: table => new + { + Id = table.Column(type: "int", nullable: false) + .Annotation("SqlServer:Identity", "1, 1"), + UserId = table.Column(type: "uniqueidentifier", nullable: false), + ClaimType = table.Column(type: "nvarchar(max)", nullable: true), + ClaimValue = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserClaims", x => x.Id); + table.ForeignKey( + name: "FK_AspNetUserClaims_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserLogins", + columns: table => new + { + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + ProviderKey = table.Column(type: "nvarchar(450)", nullable: false), + ProviderDisplayName = table.Column(type: "nvarchar(max)", nullable: true), + UserId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserLogins", x => new { x.LoginProvider, x.ProviderKey }); + table.ForeignKey( + name: "FK_AspNetUserLogins_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserRoles", + columns: table => new + { + UserId = table.Column(type: "uniqueidentifier", nullable: false), + RoleId = table.Column(type: "uniqueidentifier", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserRoles", x => new { x.UserId, x.RoleId }); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetRoles_RoleId", + column: x => x.RoleId, + principalTable: "AspNetRoles", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + table.ForeignKey( + name: "FK_AspNetUserRoles_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateTable( + name: "AspNetUserTokens", + columns: table => new + { + UserId = table.Column(type: "uniqueidentifier", nullable: false), + LoginProvider = table.Column(type: "nvarchar(450)", nullable: false), + Name = table.Column(type: "nvarchar(450)", nullable: false), + Value = table.Column(type: "nvarchar(max)", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_AspNetUserTokens", x => new { x.UserId, x.LoginProvider, x.Name }); + table.ForeignKey( + name: "FK_AspNetUserTokens_AspNetUsers_UserId", + column: x => x.UserId, + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_AspNetRoleClaims_RoleId", + table: "AspNetRoleClaims", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "RoleNameIndex", + table: "AspNetRoles", + column: "NormalizedName", + unique: true, + filter: "[NormalizedName] IS NOT NULL"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserClaims_UserId", + table: "AspNetUserClaims", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserLogins_UserId", + table: "AspNetUserLogins", + column: "UserId"); + + migrationBuilder.CreateIndex( + name: "IX_AspNetUserRoles_RoleId", + table: "AspNetUserRoles", + column: "RoleId"); + + migrationBuilder.CreateIndex( + name: "EmailIndex", + table: "AspNetUsers", + column: "NormalizedEmail"); + + migrationBuilder.CreateIndex( + name: "UserNameIndex", + table: "AspNetUsers", + column: "NormalizedUserName", + unique: true, + filter: "[NormalizedUserName] IS NOT NULL"); + + migrationBuilder.AddForeignKey( + name: "FK_TeamMembers_AspNetUsers_UserId", + table: "TeamMembers", + column: "UserId", + principalTable: "AspNetUsers", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + + migrationBuilder.AddForeignKey( + name: "FK_TeamMembers_Projects_ProjectId", + table: "TeamMembers", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropForeignKey( + name: "FK_TeamMembers_AspNetUsers_UserId", + table: "TeamMembers"); + + migrationBuilder.DropForeignKey( + name: "FK_TeamMembers_Projects_ProjectId", + table: "TeamMembers"); + + migrationBuilder.DropTable( + name: "AspNetRoleClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserClaims"); + + migrationBuilder.DropTable( + name: "AspNetUserLogins"); + + migrationBuilder.DropTable( + name: "AspNetUserRoles"); + + migrationBuilder.DropTable( + name: "AspNetUserTokens"); + + migrationBuilder.DropTable( + name: "AspNetRoles"); + + migrationBuilder.DropTable( + name: "AspNetUsers"); + + migrationBuilder.DropPrimaryKey( + name: "PK_TeamMembers", + table: "TeamMembers"); + + migrationBuilder.DropColumn( + name: "UserId", + table: "TeamMembers"); + + migrationBuilder.DropColumn( + name: "Role", + table: "TeamMembers"); + + migrationBuilder.RenameTable( + name: "TeamMembers", + newName: "TeamMember"); + + migrationBuilder.RenameIndex( + name: "IX_TeamMembers_ProjectId", + table: "TeamMember", + newName: "IX_TeamMember_ProjectId"); + + migrationBuilder.AlterColumn( + name: "ProjectId", + table: "TeamMember", + type: "uniqueidentifier", + nullable: true, + oldClrType: typeof(Guid), + oldType: "uniqueidentifier"); + + migrationBuilder.AddPrimaryKey( + name: "PK_TeamMember", + table: "TeamMember", + column: "Id"); + + migrationBuilder.AddForeignKey( + name: "FK_TeamMember_Projects_ProjectId", + table: "TeamMember", + column: "ProjectId", + principalTable: "Projects", + principalColumn: "Id"); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.Designer.cs new file mode 100644 index 0000000..628dffe --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.Designer.cs @@ -0,0 +1,597 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230406113321_SeedRoles")] + partial class SeedRoles + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Faqs"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "ProjectId"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("GameDevPortal.Core.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = new Guid("296ecfda-774c-4042-9eef-6712df53031e"), + Name = "Admin", + NormalizedName = "ADMIN" + }, + new + { + Id = new Guid("2cb05251-40f6-44fb-8366-781953860b5d"), + Name = "Moderator", + NormalizedName = "MODERATOR" + }, + new + { + Id = new Guid("22c6bd6c-8ffc-4bd2-8143-5ca03557cd99"), + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("Faq") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("TeamMembers") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", "User") + .WithMany("TeamMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("Faq"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Navigation("TeamMemberships"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.cs new file mode 100644 index 0000000..4a0cba1 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406113321_SeedRoles.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class SeedRoles : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { new Guid("22c6bd6c-8ffc-4bd2-8143-5ca03557cd99"), null, "User", "USER" }, + { new Guid("296ecfda-774c-4042-9eef-6712df53031e"), null, "Admin", "ADMIN" }, + { new Guid("2cb05251-40f6-44fb-8366-781953860b5d"), null, "Moderator", "MODERATOR" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("22c6bd6c-8ffc-4bd2-8143-5ca03557cd99")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("296ecfda-774c-4042-9eef-6712df53031e")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("2cb05251-40f6-44fb-8366-781953860b5d")); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.Designer.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.Designer.cs new file mode 100644 index 0000000..d1d83cd --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.Designer.cs @@ -0,0 +1,597 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + [Migration("20230406123200_RenameAdmin")] + partial class RenameAdmin + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Faqs"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "ProjectId"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("GameDevPortal.Core.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = new Guid("662ce9ac-052d-4211-ae0d-50883ccdf872"), + Name = "Administrator", + NormalizedName = "ADMINISTRATOR" + }, + new + { + Id = new Guid("094bf682-2c34-4d18-a24f-a20ffbd232be"), + Name = "Moderator", + NormalizedName = "MODERATOR" + }, + new + { + Id = new Guid("27a71320-dfd4-4833-bbad-5f7fc5670532"), + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("Faq") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("TeamMembers") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", "User") + .WithMany("TeamMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("Faq"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Navigation("TeamMemberships"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.cs new file mode 100644 index 0000000..06e505e --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/20230406123200_RenameAdmin.cs @@ -0,0 +1,71 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional + +namespace GameDevPortal.Infrastructure.Migrations +{ + /// + public partial class RenameAdmin : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("22c6bd6c-8ffc-4bd2-8143-5ca03557cd99")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("296ecfda-774c-4042-9eef-6712df53031e")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("2cb05251-40f6-44fb-8366-781953860b5d")); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { new Guid("094bf682-2c34-4d18-a24f-a20ffbd232be"), null, "Moderator", "MODERATOR" }, + { new Guid("27a71320-dfd4-4833-bbad-5f7fc5670532"), null, "User", "USER" }, + { new Guid("662ce9ac-052d-4211-ae0d-50883ccdf872"), null, "Administrator", "ADMINISTRATOR" } + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("094bf682-2c34-4d18-a24f-a20ffbd232be")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("27a71320-dfd4-4833-bbad-5f7fc5670532")); + + migrationBuilder.DeleteData( + table: "AspNetRoles", + keyColumn: "Id", + keyValue: new Guid("662ce9ac-052d-4211-ae0d-50883ccdf872")); + + migrationBuilder.InsertData( + table: "AspNetRoles", + columns: new[] { "Id", "ConcurrencyStamp", "Name", "NormalizedName" }, + values: new object[,] + { + { new Guid("22c6bd6c-8ffc-4bd2-8143-5ca03557cd99"), null, "User", "USER" }, + { new Guid("296ecfda-774c-4042-9eef-6712df53031e"), null, "Admin", "ADMIN" }, + { new Guid("2cb05251-40f6-44fb-8366-781953860b5d"), null, "Moderator", "MODERATOR" } + }); + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/Migrations/ProjectContextModelSnapshot.cs b/backend/GameDevPortal.Infrastructure/Data/Migrations/ProjectContextModelSnapshot.cs new file mode 100644 index 0000000..4401f11 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/Migrations/ProjectContextModelSnapshot.cs @@ -0,0 +1,594 @@ +// +using System; +using GameDevPortal.Infrastructure.Data; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +#nullable disable + +namespace GameDevPortal.Infrastructure.Migrations +{ + [DbContext(typeof(ProjectContext))] + partial class ProjectContextModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "7.0.4") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.Property("CategoriesId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProgressReportsId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("CategoriesId", "ProgressReportsId"); + + b.HasIndex("ProgressReportsId"); + + b.ToTable("CategoryProgressReport"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("HexColour") + .IsRequired() + .HasMaxLength(7) + .HasColumnType("nvarchar(7)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Categories"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Answer") + .IsRequired() + .HasMaxLength(1500) + .HasColumnType("nvarchar(1500)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Question") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("Faqs"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("MadePublicAt") + .HasColumnType("datetime2"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(150) + .HasColumnType("nvarchar(150)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("ProjectId"); + + b.ToTable("ProgressReports"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.ToTable("Projects"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Id") + .HasColumnType("uniqueidentifier"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.HasKey("UserId", "ProjectId"); + + b.HasIndex("ProjectId"); + + b.ToTable("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessFailedCount") + .HasColumnType("int"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("EmailConfirmed") + .HasColumnType("bit"); + + b.Property("IsDeleted") + .HasColumnType("bit"); + + b.Property("LockoutEnabled") + .HasColumnType("bit"); + + b.Property("LockoutEnd") + .HasColumnType("datetimeoffset"); + + b.Property("NormalizedEmail") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedUserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("PasswordHash") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PhoneNumberConfirmed") + .HasColumnType("bit"); + + b.Property("SecurityStamp") + .HasColumnType("nvarchar(max)"); + + b.Property("TwoFactorEnabled") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UserName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedEmail") + .HasDatabaseName("EmailIndex"); + + b.HasIndex("NormalizedUserName") + .IsUnique() + .HasDatabaseName("UserNameIndex") + .HasFilter("[NormalizedUserName] IS NOT NULL"); + + b.ToTable("AspNetUsers", (string)null); + }); + + modelBuilder.Entity("GameDevPortal.Core.Models.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConcurrencyStamp") + .IsConcurrencyToken() + .HasColumnType("nvarchar(max)"); + + b.Property("Name") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.Property("NormalizedName") + .HasMaxLength(256) + .HasColumnType("nvarchar(256)"); + + b.HasKey("Id"); + + b.HasIndex("NormalizedName") + .IsUnique() + .HasDatabaseName("RoleNameIndex") + .HasFilter("[NormalizedName] IS NOT NULL"); + + b.ToTable("AspNetRoles", (string)null); + + b.HasData( + new + { + Id = new Guid("662ce9ac-052d-4211-ae0d-50883ccdf872"), + Name = "Administrator", + NormalizedName = "ADMINISTRATOR" + }, + new + { + Id = new Guid("094bf682-2c34-4d18-a24f-a20ffbd232be"), + Name = "Moderator", + NormalizedName = "MODERATOR" + }, + new + { + Id = new Guid("27a71320-dfd4-4833-bbad-5f7fc5670532"), + Name = "User", + NormalizedName = "USER" + }); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetRoleClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int"); + + SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property("Id")); + + b.Property("ClaimType") + .HasColumnType("nvarchar(max)"); + + b.Property("ClaimValue") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserClaims", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderKey") + .HasColumnType("nvarchar(450)"); + + b.Property("ProviderDisplayName") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("LoginProvider", "ProviderKey"); + + b.HasIndex("UserId"); + + b.ToTable("AspNetUserLogins", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("AspNetUserRoles", (string)null); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("LoginProvider") + .HasColumnType("nvarchar(450)"); + + b.Property("Name") + .HasColumnType("nvarchar(450)"); + + b.Property("Value") + .HasColumnType("nvarchar(max)"); + + b.HasKey("UserId", "LoginProvider", "Name"); + + b.ToTable("AspNetUserTokens", (string)null); + }); + + modelBuilder.Entity("CategoryProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Category", null) + .WithMany() + .HasForeignKey("CategoriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.ProgressReport", null) + .WithMany() + .HasForeignKey("ProgressReportsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Category", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", null) + .WithMany("Categories") + .HasForeignKey("ProjectId"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Faq", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("Faq") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.ProgressReport", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("ProgressReports") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.OwnsOne("GameDevPortal.Core.ValueTypes.ProjectTimeFrame", "TimeFrame", b1 => + { + b1.Property("ProjectId") + .HasColumnType("uniqueidentifier"); + + b1.Property("EndDate") + .HasColumnType("datetime2"); + + b1.Property("StartDate") + .HasColumnType("datetime2"); + + b1.Property("Status") + .HasColumnType("int"); + + b1.HasKey("ProjectId"); + + b1.ToTable("Projects"); + + b1.WithOwner() + .HasForeignKey("ProjectId"); + }); + + b.Navigation("TimeFrame") + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.TeamMember", b => + { + b.HasOne("GameDevPortal.Core.Entities.Project", "Project") + .WithMany("TeamMembers") + .HasForeignKey("ProjectId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", "User") + .WithMany("TeamMemberships") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Project"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityRoleClaim", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserClaim", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserLogin", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserRole", b => + { + b.HasOne("GameDevPortal.Core.Models.UserRole", null) + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("Microsoft.AspNetCore.Identity.IdentityUserToken", b => + { + b.HasOne("GameDevPortal.Core.Entities.User", null) + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.Project", b => + { + b.Navigation("Categories"); + + b.Navigation("Faq"); + + b.Navigation("ProgressReports"); + + b.Navigation("TeamMembers"); + }); + + modelBuilder.Entity("GameDevPortal.Core.Entities.User", b => + { + b.Navigation("TeamMemberships"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/backend/GameDevPortal.Infrastructure/Data/ProjectContext.cs b/backend/GameDevPortal.Infrastructure/Data/ProjectContext.cs new file mode 100644 index 0000000..2dce6c8 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Data/ProjectContext.cs @@ -0,0 +1,66 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using GameDevPortal.Infrastructure.Data.Configuration; +using Microsoft.AspNetCore.Identity.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Design; +using Microsoft.Extensions.Configuration; + +namespace GameDevPortal.Infrastructure.Data; + +public class ProjectContext : IdentityDbContext +{ + public DbSet TeamMembers => Set(); + public DbSet Projects => Set(); + public DbSet ProgressReports => Set(); + public DbSet Categories => Set(); + public DbSet Faqs => Set(); + + public ProjectContext(DbContextOptions options) : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder builder) + { + new UserEntityTypeConfiguration().Configure(builder.Entity()); + new ProjectEntityTypeConfiguration().Configure(builder.Entity()); + new TeamMemberEntityTypeConfiguration().Configure(builder.Entity()); + new UserRoleEntityTypeConfiguration().Configure(builder.Entity()); + base.OnModelCreating(builder); + } + + public override async Task SaveChangesAsync(CancellationToken cancellationToken = default) + { + foreach (var entry in ChangeTracker.Entries().Where(e => e.Entity is IEntity).ToList()) + { + switch (entry.State) + { + case EntityState.Added: + var timeStamp = DateTime.UtcNow; + entry.Property(nameof(IEntity.CreatedAt)).CurrentValue = timeStamp; + entry.Property(nameof(IEntity.UpdatedAt)).CurrentValue = timeStamp; + break; + case EntityState.Modified: + entry.Property(nameof(IEntity.UpdatedAt)).CurrentValue = DateTime.UtcNow; + break; + } + } + + return await base.SaveChangesAsync(cancellationToken); + } +} + +public class ProjectContextFactory : IDesignTimeDbContextFactory +{ + public ProjectContext CreateDbContext(string[] args) + { + var optionsBuilder = new DbContextOptionsBuilder(); + + IConfigurationRoot config = new ConfigurationBuilder().AddJsonFile("appsettings.Development.json").Build(); + + optionsBuilder.UseSqlServer(config.GetConnectionString("ProjectDBConnectionString")); + + return new ProjectContext(optionsBuilder.Options); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/Files/LocalFileTemplateProvider.cs b/backend/GameDevPortal.Infrastructure/Files/LocalFileTemplateProvider.cs new file mode 100644 index 0000000..1b9052d --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Files/LocalFileTemplateProvider.cs @@ -0,0 +1,33 @@ +using GameDevPortal.Core.Interfaces.Notifications; +using Microsoft.IdentityModel.Tokens; +using Microsoft.VisualBasic.FileIO; + +namespace GameDevPortal.Infrastructure.Files; + +public class LocalFileTemplateProvider : ITemplateProvider +{ + private readonly string _templateFolderPath; + + public LocalFileTemplateProvider(string templateFolderPath = "") + { + _templateFolderPath = templateFolderPath; + if (!templateFolderPath.IsNullOrEmpty() && !FileSystem.DirectoryExists(templateFolderPath)) + { + throw new FileNotFoundException("Template folder not found.", templateFolderPath); + } + } + + public string ReadTemplate(string templateName) + { + string templatePath = Path.Combine(_templateFolderPath, templateName); + + if (!FileSystem.FileExists(templatePath)) + { + throw new FileNotFoundException("Template file not found.", templatePath); + } + + using StreamReader reader = new(templatePath); + + return reader.ReadToEnd(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/GameDevPortal.Infrastructure.csproj b/backend/GameDevPortal.Infrastructure/GameDevPortal.Infrastructure.csproj new file mode 100644 index 0000000..f25469d --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/GameDevPortal.Infrastructure.csproj @@ -0,0 +1,30 @@ + + + + net7.0 + enable + enable + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + diff --git a/backend/GameDevPortal.Infrastructure/Notifications/Configuration/SendGridConfiguration.cs b/backend/GameDevPortal.Infrastructure/Notifications/Configuration/SendGridConfiguration.cs new file mode 100644 index 0000000..d69d088 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Notifications/Configuration/SendGridConfiguration.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.Infrastructure.Notifications.Configuration; + +public class SendGridConfiguration +{ + public const string SectionName = nameof(SendGridConfiguration); + [Required] + public string ApiKey { get; set; } + [Required] + public string SenderEmail { get; set; } + [Required] + public string SenderName { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.Infrastructure/Notifications/Services/EmailNotificationService.cs b/backend/GameDevPortal.Infrastructure/Notifications/Services/EmailNotificationService.cs new file mode 100644 index 0000000..a6ee2d7 --- /dev/null +++ b/backend/GameDevPortal.Infrastructure/Notifications/Services/EmailNotificationService.cs @@ -0,0 +1,41 @@ +using GameDevPortal.Core.Models; +using SendGrid.Helpers.Mail; +using SendGrid; +using Microsoft.Extensions.Options; +using GameDevPortal.Infrastructure.Notifications.Configuration; +using GameDevPortal.Core.Models.Notifications; +using GameDevPortal.Core.Interfaces.Notifications; + +namespace GameDevPortal.Infrastructure.Notifications.Services +{ + public class EmailNotificationService : INotificationService + { + private readonly IOptionsMonitor _sendgridConfig; + + public EmailNotificationService(IOptionsMonitor sendgridConfig) + { + _sendgridConfig = sendgridConfig ?? throw new ArgumentNullException(nameof(sendgridConfig)); + } + + async Task INotificationService.Send(Notification notification) + { + string apiKey = _sendgridConfig.CurrentValue.ApiKey; + SendGridClient client = new(apiKey); + EmailAddress from = new(_sendgridConfig.CurrentValue.SenderEmail, _sendgridConfig.CurrentValue.SenderName); + string subject = notification.Title; + string plainTextContent = notification.Body; + + List recipientEmailAddresses = new(); + foreach (string recipient in notification.Recipients) + { + recipientEmailAddresses.Add(new EmailAddress(recipient)); + } + + SendGridMessage msg = MailHelper.CreateSingleEmailToMultipleRecipients(from, recipientEmailAddresses, subject, plainTextContent, string.Empty); + + Response response = await client.SendEmailAsync(msg); + + return response.IsSuccessStatusCode ? OperationResult.CreateSuccessResult() : OperationResult.CreateFailure(new Exception(response.Headers.ToString())); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Configuration/JwtConfiguration.cs b/backend/GameDevPortal.WebAPI/Configuration/JwtConfiguration.cs new file mode 100644 index 0000000..a6994c2 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Configuration/JwtConfiguration.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Configuration; + +public class JwtConfiguration +{ + public const string SectionName = "JwtSettings"; + + [Required] + public string ValidIssuer { get; set; } + + [Required] + public string ValidAudience { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/AuthenticationController.cs b/backend/GameDevPortal.WebAPI/Controllers/AuthenticationController.cs new file mode 100644 index 0000000..4538fd5 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/AuthenticationController.cs @@ -0,0 +1,55 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.Authentication; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Models.Dtos.UserDtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Primitives; + +namespace ConsoleProjectManagement.WebAPI.Controllers; + +[ApiController] +[Route("api/authentication"), Authorize] +public class AuthenticationController : ControllerBase +{ + private readonly IAuthenticationService _authManager; + private readonly ILogger _logger; + + public AuthenticationController(IAuthenticationService authManager, ILogger logger) + { + _authManager = authManager ?? throw new ArgumentNullException(nameof(authManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + [HttpPost("login"), AllowAnonymous] + public async Task Authenticate([FromBody] UserAuthenticationDto userDto) + { + var authenticationResult = await _authManager.ValidateUser(userDto.UserName, userDto.Password); + if (!authenticationResult.Success) + { + _logger.LogError($"Authentication failed, message: {authenticationResult.ErrorMessage}"); + return Forbid(); + } + + User authenticatedUser = authenticationResult.ResultData; + + var tokenGetResult = await _authManager.CreateToken(authenticatedUser); + + if (!tokenGetResult.Success) + { + _logger.LogError($"Getting token failed, message: {tokenGetResult.ErrorMessage}"); + throw tokenGetResult.Exception; + } + + TokenDto token = tokenGetResult.ResultData; + + return Ok(new { Token = token }); + } + + [HttpPost("logout")] + public ActionResult Logout() + { + return Ok(new { Token = _authManager.CreateExpiredToken() }); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/CategoryController.cs b/backend/GameDevPortal.WebAPI/Controllers/CategoryController.cs new file mode 100644 index 0000000..7f098d2 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/CategoryController.cs @@ -0,0 +1,123 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Extensions; +using GameDevPortal.WebAPI.Extensions.EntityExtensions; +using GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace GameDevPortal.WebAPI.Controllers +{ + [ApiController] + [Route("api/categories")] + public class CategoryController : ControllerBase + { + private const string _getName = $"{nameof(Get)}{nameof(Category)}"; + private readonly IMapper _mapper; + private readonly ICategoryDomainService _domainService; + + public CategoryController(IMapper mapper, ICategoryDomainService domainService) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _domainService = domainService ?? throw new ArgumentNullException(nameof(domainService)); + } + + [HttpGet] + public async Task>> List([FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.List(pagination, cancellationToken); + + if (!listResult.Success) throw listResult.Exception; + + IEnumerable entities = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(entities); + + return Ok(dtos); + } + + [HttpPost] + public async Task Insert(CategoryCreateDto createDto, CancellationToken cancellationToken = default) + { + Category entity = _mapper.Map(createDto); + + var saveResult = await _domainService.Insert(entity, cancellationToken); + + if (!saveResult.Success) throw saveResult.Exception; + + CategoryGetDto createdEntityDto = _mapper.Map(entity); + + return CreatedAtRoute(_getName, new { id = createdEntityDto.Id }, createdEntityDto); + } + + [HttpGet("{id}", Name = _getName)] + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Category entity = getResult.ResultData; + CategoryGetDto dto = _mapper.Map(entity); + + return Ok(dto); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, CategoryUpdateDto updateDto, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Category entity = getResult.ResultData; + entity.Update(updateDto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success && updateResult.Exception is KeyNotFoundException) return NotFound(); + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpPatch("{id}")] + public async Task UpdatePartial(Guid id, JsonPatchDocument patchDocument, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Category entity = getResult.ResultData; + CategoryUpdateDto dto = _mapper.Map(entity); + + patchDocument.ApplyTo(dto, ModelState); + if (!ModelState.IsValid) return BadRequest(ModelState); + if (!TryValidateModel(ModelState)) return BadRequest(ModelState); + + entity.Update(dto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + var deleteResult = await _domainService.Delete(id, cancellationToken); + + if (!deleteResult.Success && deleteResult.Exception is KeyNotFoundException) return NotFound(); + if (!deleteResult.Success) throw deleteResult.Exception; + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/FaqController.cs b/backend/GameDevPortal.WebAPI/Controllers/FaqController.cs new file mode 100644 index 0000000..68c88e9 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/FaqController.cs @@ -0,0 +1,123 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Extensions; +using GameDevPortal.WebAPI.Extensions.EntityExtensions; +using GameDevPortal.WebAPI.Models.Dtos.FaqDtos; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace GameDevPortal.WebAPI.Controllers +{ + [ApiController] + [Route("api/faq")] + public class FaqController : ControllerBase + { + private const string _getName = $"{nameof(Get)}{nameof(Faq)}"; + private readonly IMapper _mapper; + private readonly IFaqDomainService _domainService; + + public FaqController(IMapper mapper, IFaqDomainService domainService) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _domainService = domainService ?? throw new ArgumentNullException(nameof(domainService)); + } + + [HttpGet] + public async Task>> List([FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.List(pagination, cancellationToken); + + if (!listResult.Success) throw listResult.Exception; + + IEnumerable resultData = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(resultData); + + return Ok(dtos); + } + + [HttpPost] + public async Task Insert(FaqCreateDto createDto, CancellationToken cancellationToken = default) + { + Faq entity = _mapper.Map(createDto); + + var saveResult = await _domainService.Insert(entity, cancellationToken); + + if (!saveResult.Success) throw saveResult.Exception; + + FaqGetDto createdEntityDto = _mapper.Map(entity); + + return CreatedAtRoute(_getName, new { id = createdEntityDto.Id }, createdEntityDto); + } + + [HttpGet("{id}", Name = _getName)] + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Faq entity = getResult.ResultData; + FaqGetDto dto = _mapper.Map(entity); + + return Ok(dto); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, FaqUpdateDto updateDto, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Faq entity = getResult.ResultData; + entity.Update(updateDto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success && updateResult.Exception is KeyNotFoundException) return NotFound(); + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpPatch("{id}")] + public async Task UpdatePartial(Guid id, JsonPatchDocument patchDocument, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Faq entity = getResult.ResultData; + FaqUpdateDto dto = _mapper.Map(entity); + + patchDocument.ApplyTo(dto, ModelState); + if (!ModelState.IsValid) return BadRequest(ModelState); + if (!TryValidateModel(ModelState)) return BadRequest(ModelState); + + entity.Update(dto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + var deleteResult = await _domainService.Delete(id, cancellationToken); + + if (!deleteResult.Success && deleteResult.Exception is KeyNotFoundException) return NotFound(); + if (!deleteResult.Success) throw deleteResult.Exception; + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/ProgressReportController.cs b/backend/GameDevPortal.WebAPI/Controllers/ProgressReportController.cs new file mode 100644 index 0000000..dd013c4 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/ProgressReportController.cs @@ -0,0 +1,125 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Extensions; +using GameDevPortal.WebAPI.Extensions.EntityExtensions; +using GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; +using GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace GameDevPortal.WebAPI.Controllers +{ + [ApiController] + [Route("api/progress-reports")] + public class ProgressReportController : ControllerBase + { + private const string _getName = $"{nameof(Get)}{nameof(ProgressReport)}"; + private readonly IMapper _mapper; + private readonly IProgressReportDomainService _domainService; + + public ProgressReportController(IMapper mapper, IProgressReportDomainService domainService) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _domainService = domainService ?? throw new ArgumentNullException(nameof(domainService)); + } + + [HttpGet] + public async Task>> List([FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.List(pagination, cancellationToken); + + if (!listResult.Success) throw listResult.Exception; + + IEnumerable entities = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(entities); + + return Ok(dtos); + } + + [HttpPost] + public async Task Insert(ProgressReportCreateDto createDto, CancellationToken cancellationToken = default) + { + ProgressReport entity = _mapper.Map(createDto); + + var saveResult = await _domainService.Insert(entity, cancellationToken); + + if (!saveResult.Success && saveResult.Exception is KeyNotFoundException) return NotFound(saveResult.Exception.Message); + else if (!saveResult.Success) throw saveResult.Exception; + + ProgressReportGetDto createdEntityDto = _mapper.Map(entity); + + return CreatedAtRoute(_getName, new { id = createdEntityDto.Id }, createdEntityDto); + } + + [HttpGet("{id}", Name = _getName)] + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + ProgressReport entity = getResult.ResultData; + ProgressReportGetDto dto = _mapper.Map(entity); + + return Ok(dto); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, ProgressReportUpdateDto updateDto, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + ProgressReport entity = getResult.ResultData; + entity.Update(updateDto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success && updateResult.Exception is KeyNotFoundException) return NotFound(); + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpPatch("{id}")] + public async Task UpdatePartial(Guid id, JsonPatchDocument patchDocument, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + ProgressReport entity = getResult.ResultData; + ProgressReportUpdateDto dto = _mapper.Map(entity); + + patchDocument.ApplyTo(dto, ModelState); + if (!ModelState.IsValid) return BadRequest(ModelState); + if (!TryValidateModel(ModelState)) return BadRequest(ModelState); + + entity.Update(dto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + var deleteResult = await _domainService.Delete(id, cancellationToken); + + if (!deleteResult.Success && deleteResult.Exception is KeyNotFoundException) return NotFound(); + if (!deleteResult.Success) throw deleteResult.Exception; + + return NoContent(); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/ProjectController.cs b/backend/GameDevPortal.WebAPI/Controllers/ProjectController.cs new file mode 100644 index 0000000..4f7db71 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/ProjectController.cs @@ -0,0 +1,152 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Extensions.EntityExtensions; +using GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; +using GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; +using GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; +using Microsoft.AspNetCore.JsonPatch; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; + +namespace GameDevPortal.WebAPI.Controllers +{ + [ApiController] + [Route("api/projects")] + public class ProjectController : ControllerBase + { + private const string _getName = $"{nameof(Get)}{nameof(Project)}"; + private readonly IMapper _mapper; + private readonly IProjectDomainService _domainService; + + public ProjectController(IMapper mapper, IProjectDomainService domainService) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _domainService = domainService ?? throw new ArgumentNullException(nameof(domainService)); + } + + [HttpGet] + public async Task>> List([FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.List(pagination, cancellationToken); + + if (!listResult.Success) throw listResult.Exception; + + IEnumerable entities = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(entities); + + return Ok(dtos); + } + + [HttpPost] + public async Task Insert(ProjectCreateDto createDto, CancellationToken cancellationToken = default) + { + Project entity = _mapper.Map(createDto); + + var saveResult = await _domainService.Insert(entity, cancellationToken); + + if (!saveResult.Success) throw saveResult.Exception; + + ProjectGetDto createdEntityDto = _mapper.Map(entity); + + return CreatedAtRoute(_getName, new { id = createdEntityDto.Id }, createdEntityDto); + } + + [HttpGet("{id}", Name = _getName)] + public async Task> Get(Guid id, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Project entity = getResult.ResultData; + ProjectGetDto dto = _mapper.Map(entity); + + return Ok(dto); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, ProjectUpdateDto updateDto, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Project entity = getResult.ResultData; + entity.Update(updateDto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success && updateResult.Exception is KeyNotFoundException) return NotFound(); + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpPatch("{id}")] + public async Task UpdatePartial(Guid id, JsonPatchDocument patchDocument, CancellationToken cancellationToken = default) + { + var getResult = await _domainService.Get(id, cancellationToken); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + Project entity = getResult.ResultData; + ProjectUpdateDto dto = _mapper.Map(entity); + + patchDocument.ApplyTo(dto, ModelState); + if (!ModelState.IsValid) return BadRequest(ModelState); + if (!TryValidateModel(ModelState)) return BadRequest(ModelState); + + entity.Update(dto); + + var updateResult = await _domainService.Update(entity, cancellationToken); + + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(Guid id, CancellationToken cancellationToken = default) + { + var deleteResult = await _domainService.Delete(id, cancellationToken); + + if (!deleteResult.Success && deleteResult.Exception is KeyNotFoundException) return NotFound(); + if (!deleteResult.Success) throw deleteResult.Exception; + + return NoContent(); + } + + [HttpGet("{id}/progress-reports")] + public async Task>> ListProgressReports(Guid id, [FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.ListProgressReports(id, pagination, cancellationToken); + + if (!listResult.Success && listResult.Exception is KeyNotFoundException) return NotFound(); + if (!listResult.Success) throw listResult.Exception; + + IEnumerable entities = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(entities); + + return Ok(dtos); + } + + [HttpGet("{id}/categories")] + public async Task>> ListCategories(Guid id, [FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _domainService.ListCategories(id, pagination, cancellationToken); + + if (!listResult.Success && listResult.Exception is KeyNotFoundException) return NotFound(); + if (!listResult.Success) throw listResult.Exception; + + IEnumerable entities = listResult.ResultData; + IEnumerable dtos = _mapper.Map>(entities); + + return Ok(dtos); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Controllers/UsersController.cs b/backend/GameDevPortal.WebAPI/Controllers/UsersController.cs new file mode 100644 index 0000000..3808f44 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Controllers/UsersController.cs @@ -0,0 +1,205 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.Authentication; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Extensions.EntityExtensions; +using GameDevPortal.WebAPI.Models.Dtos.UserDtos; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using System.Runtime.InteropServices; +using System.Security.Claims; +using System.Text.RegularExpressions; + +namespace ConsoleProjectManagement.WebAPI.Controllers; + +[ApiController] +[Route("api/users"), Authorize] +public class UsersController : ControllerBase +{ + private readonly IMapper _mapper; + private readonly IUserService _userService; + + public UsersController(IMapper mapper, IUserService userService) + { + _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + } + + [HttpGet, Authorize(Roles = "Administrator")] + public async Task>> List([FromQuery][Optional] Pagination pagination, CancellationToken cancellationToken = default) + { + var listResult = await _userService.List(pagination, cancellationToken); + + if (!listResult.Success) throw listResult.Exception; + + IEnumerable users = listResult.ResultData; + List userDtos = new(); + + foreach (User u in users) + { + var getRolesResult = await _userService.GetRoles(u); + + if (!getRolesResult.Success) throw getRolesResult.Exception; + + IEnumerable roles = getRolesResult.ResultData; + + UserGetDto userDto = _mapper.Map(u); + userDto.Roles = roles; + + userDtos.Add(userDto); + } + + return Ok(userDtos); + } + + [HttpPost, Authorize(Roles = "Administrator")] + public async Task Create([FromBody] UserCreationDto dto) + { + User user = _mapper.Map(dto); + + var createResult = await _userService.Create(user, dto.Password, dto.Roles); + + if (!createResult.Success) throw createResult.Exception; + + return StatusCode(201); + } + + [HttpGet("{id}")] + public async Task>> Get(Guid id) + { + var userId = User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value; + var userRole = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; + + // TODO: Use a policy to do this together with an IAuthorizationService + if (id.ToString() != userId && userRole != "Administrator") + { + return Unauthorized("You are not authorized to access this account's information."); + } + + var getResult = await _userService.Get(id); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + User user = getResult.ResultData; + + var getRolesResult = await _userService.GetRoles(user); + + if (!getRolesResult.Success) throw getRolesResult.Exception; + + IEnumerable roles = getRolesResult.ResultData; + + UserGetDto userDto = _mapper.Map(user); + userDto.Roles = roles; + + return Ok(userDto); + } + + [HttpPut("{id}")] + public async Task Update(Guid id, UserUpdateDto dto) + { + var userId = User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value; + var userRole = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; + + if (id.ToString() != userId && userRole != "Administrator") + { + return Unauthorized("You are not authorized to make changes to this account."); + } + + var getResult = await _userService.Get(id); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + User user = getResult.ResultData; + user.Update(dto); + + var updateResult = await _userService.Update(user); + + if (!updateResult.Success) throw updateResult.Exception; + + return NoContent(); + } + + [HttpDelete("{id}")] + public async Task Delete(Guid id) + { + var userId = User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value; + var userRole = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; + + if (id.ToString() != userId && userRole != "Administrator") + { + return Unauthorized("You are not authorized to delete this account."); + } + + var deleteResult = await _userService.Delete(id); + + if (!deleteResult.Success) throw deleteResult.Exception; + + return NoContent(); + } + + [HttpPost("register"), AllowAnonymous] + public async Task Register([FromBody] UserRegistrationDto dto) + { + User user = _mapper.Map(dto); + + var registerResult = await _userService.Register(user, dto.Password); + + if (!registerResult.Success) throw registerResult.Exception; + + return StatusCode(201); + } + + [HttpPut("set-roles/{id}"), Authorize(Roles = "Administrator")] + public async Task SetRoles(Guid id, IEnumerable roles) + { + var getResult = await _userService.Get(id); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + User user = getResult.ResultData; + + var setRolesResult = await _userService.SetRoles(user, roles); + + if (!setRolesResult.Success && setRolesResult.Exception is KeyNotFoundException) return NotFound($"No user with id {id} was found."); + if (!setRolesResult.Success && setRolesResult.Exception is InvalidOperationException) + { + Regex rx = new Regex(@"^Role (?\w+) does not exist\.$"); + Match match = rx.Match(setRolesResult.Exception.Message); + + if (match.Success) + return NotFound($"Role {match.Groups["role"]} does not exist."); + } + if (!setRolesResult.Success) throw setRolesResult.Exception; + + return NoContent(); + } + + [HttpPost("change-password/{id}")] + public async Task ChangePassword(Guid id, string currentPassword, string newPassword) + { + var userId = User.Claims.FirstOrDefault(c => c.Type == "UserId")?.Value; + var userRole = User.Claims.FirstOrDefault(c => c.Type == ClaimTypes.Role)?.Value; + + if (id.ToString() != userId && userRole != "Administrator") + { + return Unauthorized("You are not authorized to change another user's password."); + } + + var getResult = await _userService.Get(id); + + if (!getResult.Success && getResult.Exception is KeyNotFoundException) return NotFound(); + if (!getResult.Success) throw getResult.Exception; + + User user = getResult.ResultData; + + var changePasswordResult = await _userService.ChangePassword(user, currentPassword, newPassword); + + if (!changePasswordResult.Success && changePasswordResult.Exception is InvalidOperationException) return Forbid(); + if (!changePasswordResult.Success) throw changePasswordResult.Exception; + + return NoContent(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/CategoryExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/CategoryExtensions.cs new file mode 100644 index 0000000..13d9e97 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/CategoryExtensions.cs @@ -0,0 +1,12 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; + +namespace GameDevPortal.WebAPI.Extensions.EntityExtensions; + +public static class CategoryExtensions +{ + public static void Update(this Category category, CategoryUpdateDto dto) + { + category.SetChangableValues(dto.Name, dto.HexColour); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/FaqExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/FaqExtensions.cs new file mode 100644 index 0000000..e9ba06c --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/FaqExtensions.cs @@ -0,0 +1,12 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.FaqDtos; + +namespace GameDevPortal.WebAPI.Extensions.EntityExtensions; + +public static class FaqExtensions +{ + public static void Update(this Faq faq, FaqUpdateDto dto) + { + faq.SetChangableValues(dto.Question, dto.Answer); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProgressReportExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProgressReportExtensions.cs new file mode 100644 index 0000000..bc5f042 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProgressReportExtensions.cs @@ -0,0 +1,12 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; + +namespace GameDevPortal.WebAPI.Extensions.EntityExtensions; + +public static class ProgressReportExtensions +{ + public static void Update(this ProgressReport progressReport, ProgressReportUpdateDto dto) + { + progressReport.SetChangableValues(dto.Title, dto.Description, dto.MadePublicAt, dto.CategoryIds); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProjectExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProjectExtensions.cs new file mode 100644 index 0000000..8720bcb --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/ProjectExtensions.cs @@ -0,0 +1,12 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; + +namespace GameDevPortal.WebAPI.Extensions.EntityExtensions; + +public static class ProjectExtensions +{ + public static void Update(this Project project, ProjectUpdateDto dto) + { + project.SetChangableValues(dto.Name, dto.Description, dto.TimeFrame); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/UserExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/UserExtensions.cs new file mode 100644 index 0000000..26e922d --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/EntityExtensions/UserExtensions.cs @@ -0,0 +1,13 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; +using GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +namespace GameDevPortal.WebAPI.Extensions.EntityExtensions; + +public static class UserExtensions +{ + public static void Update(this User user, UserUpdateDto dto) + { + user.SetChangableValues(dto.UserName); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/ExceptionMiddlewareExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/ExceptionMiddlewareExtensions.cs new file mode 100644 index 0000000..cd64125 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/ExceptionMiddlewareExtensions.cs @@ -0,0 +1,11 @@ +using GameDevPortal.WebAPI.Middlewares; + +namespace GameDevPortal.WebAPI.Extensions; + +public static class ExceptionMiddlewareExtensions +{ + public static void ConfigureCustomExceptionMiddleware(this IApplicationBuilder app) + { + app.UseMiddleware(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Extensions/ServiceExtensions.cs b/backend/GameDevPortal.WebAPI/Extensions/ServiceExtensions.cs new file mode 100644 index 0000000..66a2718 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Extensions/ServiceExtensions.cs @@ -0,0 +1,47 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Models; +using GameDevPortal.Infrastructure.Data; +using Microsoft.AspNetCore.Authentication.JwtBearer; +using Microsoft.AspNetCore.Identity; +using Microsoft.IdentityModel.Tokens; +using System.Text; + +namespace GameDevPortal.WebAPI.Extensions; + +public static class ServiceExtensions +{ + public static void ConfigureIdentity(this IServiceCollection services) + { + var builder = services.AddIdentityCore(o => { + o.Password.RequireDigit = true; + o.Password.RequireLowercase = false; + o.Password.RequireUppercase = false; + o.Password.RequireNonAlphanumeric = false; + o.Password.RequiredLength = 10; + o.User.RequireUniqueEmail = true; + }); + + builder = new IdentityBuilder(builder.UserType, typeof(UserRole), builder.Services); + builder.AddEntityFrameworkStores().AddDefaultTokenProviders(); + } + + public static void ConfigureJWT(this IServiceCollection services, IConfiguration configuration) + { + var jwtSettings = configuration.GetSection("JwtSettings"); + var secretKey = Environment.GetEnvironmentVariable("JwtSignKey"); + services.AddAuthentication(opt => { + opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }).AddJwtBearer(options => { + options.TokenValidationParameters = new TokenValidationParameters { + ValidateIssuer = true, + ValidateAudience = true, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = jwtSettings.GetSection("validIssuer").Value, + ValidAudience = jwtSettings.GetSection("validAudience").Value, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey!)) + }; + }); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/GameDevPortal.WebAPI.csproj b/backend/GameDevPortal.WebAPI/GameDevPortal.WebAPI.csproj new file mode 100644 index 0000000..f414eab --- /dev/null +++ b/backend/GameDevPortal.WebAPI/GameDevPortal.WebAPI.csproj @@ -0,0 +1,40 @@ + + + + net7.0 + enable + enable + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + Always + + + + + + PreserveNewest + + + + diff --git a/backend/GameDevPortal.WebAPI/Middlewares/ExceptionMiddleware.cs b/backend/GameDevPortal.WebAPI/Middlewares/ExceptionMiddleware.cs new file mode 100644 index 0000000..f8e1cef --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Middlewares/ExceptionMiddleware.cs @@ -0,0 +1,41 @@ +using Newtonsoft.Json; +using System.Net; + +namespace GameDevPortal.WebAPI.Middlewares; + +public class ExceptionMiddleware +{ + private readonly RequestDelegate _next; + private readonly ILogger _logger; + + public ExceptionMiddleware(RequestDelegate next, ILogger logger) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task InvokeAsync(HttpContext context) + { + try + { + await _next(context); + } + catch (Exception ex) + { + _logger.LogError($"Something went wrong: {ex}"); + context.Response.ContentType = "application/json"; + + context.Response.StatusCode = ex switch + { + ArgumentException => (int)HttpStatusCode.BadRequest, + KeyNotFoundException => (int)HttpStatusCode.NotFound, + _ => (int)HttpStatusCode.InternalServerError + }; + + var result = JsonConvert.SerializeObject(new { message = ex?.Message }); + await context.Response.WriteAsync(result); + + _logger.LogError($"Global error catch: {0}", ex?.Message); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryCreateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryCreateDto.cs new file mode 100644 index 0000000..c9f7bc8 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryCreateDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; + +public class CategoryCreateDto +{ + [MaxLength(50)] + public string Name { get; set; } + + [MaxLength(7)] + public string HexColour { get; set; } + + public Guid? ProjectId { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryGetDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryGetDto.cs new file mode 100644 index 0000000..5b8bfcd --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryGetDto.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; + +public class CategoryGetDto +{ + public Guid Id { get; set; } + [MaxLength(50)] + public string Name { get; set; } + + [MaxLength(7)] + public string HexColour { get; set; } + + public Guid? ProjectId { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryUpdateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryUpdateDto.cs new file mode 100644 index 0000000..07a2600 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/CategoryDtos/CategoryUpdateDto.cs @@ -0,0 +1,14 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; + +public class CategoryUpdateDto +{ + [MaxLength(50)] + public string Name { get; set; } + + [MaxLength(7)] + public string HexColour { get; set; } + + public Guid? ProjectId { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqCreateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqCreateDto.cs new file mode 100644 index 0000000..758f462 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqCreateDto.cs @@ -0,0 +1,13 @@ +using GameDevPortal.Core.Entities; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.FaqDtos; + +public class FaqCreateDto +{ + [MaxLength(150)] + public string Question { get; set; } + [MaxLength(1500)] + public string Answer { get; set; } + public Guid ProjectId { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqGetDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqGetDto.cs new file mode 100644 index 0000000..18f1fa9 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqGetDto.cs @@ -0,0 +1,15 @@ +using GameDevPortal.Core.Entities; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.FaqDtos; + +public class FaqGetDto +{ + public Guid Id { get; set; } + [MaxLength(150)] + public string Question { get; set; } + [MaxLength(1500)] + public string Answer { get; set; } + + public Guid ProjectId { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqUpdateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqUpdateDto.cs new file mode 100644 index 0000000..5e2da02 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/FaqDtos/FaqUpdateDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.FaqDtos; + +public class FaqUpdateDto +{ + [MaxLength(150)] + public string Question { get; set; } + [MaxLength(1500)] + public string Answer { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportCreateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportCreateDto.cs new file mode 100644 index 0000000..38c6f54 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportCreateDto.cs @@ -0,0 +1,15 @@ +using GameDevPortal.Core.Entities; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; + +public class ProgressReportCreateDto +{ + [MaxLength(150)] + public string Title { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public DateTime MadePublicAt { get; set; } + public Guid ProjectId { get; set; } + public List CategoryIds { get; set; } +} diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportGetDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportGetDto.cs new file mode 100644 index 0000000..818f448 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportGetDto.cs @@ -0,0 +1,17 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; + +public class ProgressReportGetDto +{ + public Guid Id { get; set; } + [MaxLength(150)] + public string Title { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public DateTime MadePublicAt { get; set; } + public Guid ProjectId { get; set; } + public List Categories { get; set; } +} diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportUpdateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportUpdateDto.cs new file mode 100644 index 0000000..f068279 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProgressReportDtos/ProgressReportUpdateDto.cs @@ -0,0 +1,15 @@ +using GameDevPortal.Core.Entities; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; + +public class ProgressReportUpdateDto +{ + [MaxLength(150)] + public string Title { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public DateTime MadePublicAt { get; set; } + public Guid ProjectId { get; set; } + public List CategoryIds { get; set; } +} diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectCreateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectCreateDto.cs new file mode 100644 index 0000000..0591291 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectCreateDto.cs @@ -0,0 +1,14 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.ValueTypes; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; + +public class ProjectCreateDto +{ + [MaxLength(100)] + public string Name { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public ProjectTimeFrame TimeFrame { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectGetDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectGetDto.cs new file mode 100644 index 0000000..ece2ee2 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectGetDto.cs @@ -0,0 +1,16 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.ValueTypes; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; + +public class ProjectGetDto +{ + public Guid Id { get; set; } + + [MaxLength(100)] + public string Name { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public ProjectTimeFrame TimeFrame { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectUpdateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectUpdateDto.cs new file mode 100644 index 0000000..2cd765a --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/ProjectDtos/ProjectUpdateDto.cs @@ -0,0 +1,14 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.ValueTypes; +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; + +public class ProjectUpdateDto +{ + [MaxLength(100)] + public string Name { get; set; } + [MaxLength(500)] + public string Description { get; set; } + public ProjectTimeFrame TimeFrame { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserAuthenticationDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserAuthenticationDto.cs new file mode 100644 index 0000000..f8ac047 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserAuthenticationDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +public class UserAuthenticationDto +{ + [Required] + public string UserName { get; set; } + + [Required] + public string Password { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserCreationDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserCreationDto.cs new file mode 100644 index 0000000..4f8c2dd --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserCreationDto.cs @@ -0,0 +1,13 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +public class UserCreationDto +{ + [Required] + public string UserName { get; set; } + [Required] + public string Password { get; set; } + public string Email { get; set; } + public ICollection Roles { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserGetDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserGetDto.cs new file mode 100644 index 0000000..c8017a4 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserGetDto.cs @@ -0,0 +1,11 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +public class UserGetDto +{ + public Guid Id { get; set; } + public string UserName { get; set; } + public string Email { get; set; } + public IEnumerable Roles { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserRegistrationDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserRegistrationDto.cs new file mode 100644 index 0000000..9be3d83 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserRegistrationDto.cs @@ -0,0 +1,12 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +public class UserRegistrationDto +{ + [Required] + public string UserName { get; set; } + [Required] + public string Password { get; set; } + public string Email { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserUpdateDto.cs b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserUpdateDto.cs new file mode 100644 index 0000000..dc414f9 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/Dtos/UserDtos/UserUpdateDto.cs @@ -0,0 +1,8 @@ +using System.ComponentModel.DataAnnotations; + +namespace GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +public class UserUpdateDto +{ + public string UserName { get; set; } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Models/ErrorDetails.cs b/backend/GameDevPortal.WebAPI/Models/ErrorDetails.cs new file mode 100644 index 0000000..fd95a3d --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Models/ErrorDetails.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; + +namespace GameDevPortal.WebAPI.Models; + +public record ErrorDetails +{ + public int StatusCode { get; set; } + public string Message { get; set; } + + public override string ToString() + { + return JsonConvert.SerializeObject(this); + } +} diff --git a/backend/GameDevPortal.WebAPI/Profiles/CategoryProfile.cs b/backend/GameDevPortal.WebAPI/Profiles/CategoryProfile.cs new file mode 100644 index 0000000..f559d3f --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Profiles/CategoryProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.CategoryDtos; + +namespace GameDevPortal.WebAPI.Profiles; + +public class CategoryProfile : Profile +{ + public CategoryProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Profiles/FaqProfile.cs b/backend/GameDevPortal.WebAPI/Profiles/FaqProfile.cs new file mode 100644 index 0000000..8085df9 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Profiles/FaqProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.FaqDtos; + +namespace GameDevPortal.WebAPI.Profiles; + +public class FaqProfile : Profile +{ + public FaqProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Profiles/ProgressReportProfile.cs b/backend/GameDevPortal.WebAPI/Profiles/ProgressReportProfile.cs new file mode 100644 index 0000000..7cc5259 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Profiles/ProgressReportProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.ProgressReportDtos; + +namespace GameDevPortal.WebAPI.Profiles; + +public class ProgressReportProfile : Profile +{ + public ProgressReportProfile() + { + CreateMap().ConstructUsing(s => new ProgressReport(s.Title, s.Description, s.MadePublicAt, s.ProjectId, s.CategoryIds)).ForMember(dest => dest.CategoryIds, opt => opt.Ignore()); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Profiles/ProjectProfile.cs b/backend/GameDevPortal.WebAPI/Profiles/ProjectProfile.cs new file mode 100644 index 0000000..a3ee64c --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Profiles/ProjectProfile.cs @@ -0,0 +1,16 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.ProjectDtos; + +namespace GameDevPortal.WebAPI.Profiles; + +public class ProjectProfile : Profile +{ + public ProjectProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Profiles/UserProfile.cs b/backend/GameDevPortal.WebAPI/Profiles/UserProfile.cs new file mode 100644 index 0000000..e17b855 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Profiles/UserProfile.cs @@ -0,0 +1,15 @@ +using AutoMapper; +using GameDevPortal.Core.Entities; +using GameDevPortal.WebAPI.Models.Dtos.UserDtos; + +namespace GameDevPortal.WebAPI.Profiles; + +public class UserProfile : Profile +{ + public UserProfile() + { + CreateMap(); + CreateMap(); + CreateMap(); + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Program.cs b/backend/GameDevPortal.WebAPI/Program.cs new file mode 100644 index 0000000..17988f9 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Program.cs @@ -0,0 +1,97 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Interfaces.Authentication; +using GameDevPortal.Core.Interfaces.Notifications; +using GameDevPortal.Core.Interfaces.Repositories; +using GameDevPortal.Core.Services.DomainServices; +using GameDevPortal.Infrastructure.Data; +using GameDevPortal.Infrastructure.Files; +using GameDevPortal.Infrastructure.Notifications.Configuration; +using GameDevPortal.Infrastructure.Notifications.Services; +using GameDevPortal.WebAPI.Configuration; +using GameDevPortal.WebAPI.Extensions; +using GameDevPortal.WebAPI.Services; +using Microsoft.EntityFrameworkCore; +using Microsoft.OpenApi.Models; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. +builder.Services.AddControllers().AddNewtonsoftJson(); + +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => +{ + c.SwaggerDoc("v1", new OpenApiInfo + { + Title = "JWTToken_Auth_API", + Version = "v1" + }); + c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme() + { + Name = "Authorization", + Type = SecuritySchemeType.ApiKey, + Scheme = "Bearer", + BearerFormat = "JWT", + In = ParameterLocation.Header, + Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 1safsfsdfdfd\"", + }); + c.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" } }, Array.Empty() } }); +} +); + +// Setup DB +builder.Services.AddDbContext(options => + options.UseSqlServer(builder.Configuration.GetConnectionString("ProjectDBConnectionString"), x => + { + x.MigrationsAssembly("GameDevPortal.Infrastructure"); + x.EnableRetryOnFailure(); + })); + +// Setup Authentication +builder.Services.AddAuthentication(); +builder.Services.ConfigureIdentity(); +builder.Services.ConfigureJWT(builder.Configuration); + +// Setup DI +builder.Services.AddSingleton>(); +builder.Services.AddScoped(); +builder.Services.AddAutoMapper(AppDomain.CurrentDomain.GetAssemblies()); + +// Domain Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Notification Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Authentication Services +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +// Setup Config +builder.Services.Configure(builder.Configuration.GetSection(SendGridConfiguration.SectionName)); +builder.Services.Configure(builder.Configuration.GetSection(JwtConfiguration.SectionName)); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.ConfigureCustomExceptionMiddleware(); + +app.UseHttpsRedirection(); + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Properties/launchSettings.json b/backend/GameDevPortal.WebAPI/Properties/launchSettings.json new file mode 100644 index 0000000..36337e1 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:31391", + "sslPort": 44350 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5059", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7154;http://localhost:5059", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/backend/GameDevPortal.WebAPI/Services/AuthenticationService.cs b/backend/GameDevPortal.WebAPI/Services/AuthenticationService.cs new file mode 100644 index 0000000..2a5d44b --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Services/AuthenticationService.cs @@ -0,0 +1,120 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.Authentication; +using GameDevPortal.Core.Models; +using GameDevPortal.WebAPI.Configuration; +using Microsoft.Extensions.Options; +using Microsoft.IdentityModel.Tokens; +using System.IdentityModel.Tokens.Jwt; +using System.Security.Claims; +using System.Text; + +namespace GameDevPortal.WebAPI.Services; + +public class AuthenticationService : IAuthenticationService +{ + private readonly IUserService _userService; + private readonly IOptionsMonitor _jwtConfig; + + public AuthenticationService(IUserService userService, IOptionsMonitor jwtConfig) + { + _userService = userService ?? throw new ArgumentNullException(nameof(userService)); + _jwtConfig = jwtConfig ?? throw new ArgumentNullException(nameof(jwtConfig)); + } + + public async Task> ValidateUser(string username, string password) + { + var findUserResult = await _userService.Find(username); + + if (!findUserResult.Success) + { + return OperationResult.CreateFailure(new KeyNotFoundException($"No user with username: {username} was found.")); + } + + User user = findUserResult.ResultData; + + var checkPasswordResult = await _userService.ValidatePassword(user, password); + + if (!checkPasswordResult.Success) throw checkPasswordResult.Exception; + + bool passwordCorrect = checkPasswordResult.ResultData; + + if (!passwordCorrect) + { + return OperationResult.CreateFailure(new InvalidOperationException($"Entered password for user {username} was incorrect.")); + } + + return OperationResult.CreateSuccessResult(user); + } + + public async Task> CreateToken(User user) + { + var signingCredentials = GetSigningCredentials(); + + var getClaimsResult = await GetClaims(user); + + if (!getClaimsResult.Success) + { + return OperationResult.CreateFailure(getClaimsResult.Exception); + } + + List claims = getClaimsResult.ResultData; + + JwtSecurityToken tokenOptions = GenerateTokenOptions(signingCredentials, claims, 60); + JwtSecurityTokenHandler tokenHandler = new(); + string accessToken = tokenHandler.WriteToken(tokenOptions); + + TokenDto token = new(accessToken); + + return OperationResult.CreateSuccessResult(token); + } + + public string CreateExpiredToken() + { + var signingCredentials = GetSigningCredentials(); + var tokenOptions = GenerateTokenOptions(signingCredentials, new List(), -1); + return new JwtSecurityTokenHandler().WriteToken(tokenOptions); + } + + private SigningCredentials GetSigningCredentials() + { + var key = Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("JwtSignKey")!); + var secret = new SymmetricSecurityKey(key); + return new SigningCredentials(secret, SecurityAlgorithms.HmacSha256); + } + + private async Task>> GetClaims(User user) + { + var claims = new List { + new Claim(ClaimTypes.Name, user.UserName!), + new Claim("UserId", user.Id.ToString()) + }; + + var getRolesResult = await _userService.GetRoles(user); + if (!getRolesResult.Success) + { + return OperationResult>.CreateFailure(getRolesResult.Exception); + } + + var roles = getRolesResult.ResultData; + foreach (var role in roles) + { + claims.Add(new Claim(ClaimTypes.Role, role)); + } + + return OperationResult>.CreateSuccessResult(claims); + } + + private JwtSecurityToken GenerateTokenOptions(SigningCredentials signingCredentials, List claims, int validMinutes) + { + var jwtSettings = _jwtConfig.CurrentValue; + + var tokenOptions = new JwtSecurityToken( + issuer: jwtSettings.ValidIssuer, + audience: jwtSettings.ValidAudience, + claims: claims, + expires: DateTime.Now.AddMinutes(validMinutes), + signingCredentials: signingCredentials); + + return tokenOptions; + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/Services/UserService.cs b/backend/GameDevPortal.WebAPI/Services/UserService.cs new file mode 100644 index 0000000..65fb267 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/Services/UserService.cs @@ -0,0 +1,236 @@ +using GameDevPortal.Core.Entities; +using GameDevPortal.Core.Interfaces.Authentication; +using GameDevPortal.Core.Models; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace GameDevPortal.WebAPI.Services; + +public class UserService : IUserService +{ + private readonly UserManager _userManager; + private readonly ILogger _logger; + + public UserService(UserManager userManager, ILogger logger) + { + _userManager = userManager ?? throw new ArgumentNullException(nameof(userManager)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + public async Task>> List(Pagination pagination, CancellationToken cancellationToken = default) + { + try + { + IEnumerable users = await _userManager.Users.OrderBy(u => u.Id).Skip(pagination.Size * pagination.Index).Take(pagination.Size).ToListAsync(cancellationToken); + + return OperationResult>.CreateSuccessResult(users); + } + catch (Exception ex) + { + return OperationResult>.CreateFailure(ex); + } + } + + public async Task Create(User user, string password, IEnumerable roles) + { + try + { + var createResult = await _userManager.CreateAsync(user, password); + + if (!createResult.Succeeded) + { + string aggregatedError = createResult.Errors.Select(e => $"{e.Code} {e.Description}").Aggregate((s1, s2) => $"{s1} {s2}"); + _logger.LogError(aggregatedError); + return OperationResult.CreateFailure(new Exception(aggregatedError)); + } + + var addRolesResult = await _userManager.AddToRolesAsync(user, roles); + if (!addRolesResult.Succeeded) + { + string aggregatedError = addRolesResult.Errors.Select(e => $"{e.Code} {e.Description}").Aggregate((s1, s2) => $"{s1} {s2}"); + _logger.LogError(aggregatedError); + return OperationResult.CreateFailure(new Exception(aggregatedError)); + } + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task> Get(Guid id) + { + try + { + User? user = await _userManager.FindByIdAsync(id.ToString()); + + if (user is null) + { + return OperationResult.CreateFailure(new KeyNotFoundException($"No user with id {id} was found.")); + } + + return OperationResult.CreateSuccessResult(user); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task Update(User user) + { + try + { + await _userManager.UpdateAsync(user); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task Delete(Guid id) + { + try + { + User user = await _userManager.FindByIdAsync(id.ToString()) ?? throw new KeyNotFoundException($"No user with Id: {id} was found."); + await _userManager.DeleteAsync(user); + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult>.CreateFailure(ex); + } + } + + public async Task> ValidatePassword(User user, string password) + { + try + { + bool passwordCorrect = await _userManager.CheckPasswordAsync(user, password); + + return OperationResult.CreateSuccessResult(passwordCorrect); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task>> GetRoles(User user) + { + try + { + IList roles = await _userManager.GetRolesAsync(user); + + return OperationResult>.CreateSuccessResult(roles); + } + catch (Exception ex) + { + return OperationResult>.CreateFailure(ex); + } + } + + public async Task> Find(string username) + { + try + { + User? user = await _userManager.FindByNameAsync(username); + + if (user is null) + { + return OperationResult.CreateFailure(new KeyNotFoundException()); + } + + return OperationResult.CreateSuccessResult(user); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task Register(User user, string password) + { + try + { + var createResult = await _userManager.CreateAsync(user, password); + if (!createResult.Succeeded) + { + string aggregatedError = createResult.Errors.Select(e => $"{e.Code} {e.Description}").Aggregate((s1, s2) => $"{s1} {s2}"); + _logger.LogError(aggregatedError); + return OperationResult.CreateFailure(new Exception(aggregatedError)); + } + + var addRolesResult = await _userManager.AddToRolesAsync(user, new[] { "User" }); + if (!addRolesResult.Succeeded) + { + string aggregatedError = addRolesResult.Errors.Select(e => $"{e.Code} {e.Description}").Aggregate((s1, s2) => $"{s1} {s2}"); + _logger.LogError(aggregatedError); + return OperationResult.CreateFailure(new Exception(aggregatedError)); + } + + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task SetRoles(User user, IEnumerable roles) + { + try + { + var currentRoles = await _userManager.GetRolesAsync(user); + + var rolesToRemove = currentRoles.Except(roles); + if (rolesToRemove.Any()) + { + await _userManager.RemoveFromRolesAsync(user, rolesToRemove); + } + + var newRoles = roles.Except(currentRoles); + if (newRoles.Any()) + { + try + { + await _userManager.AddToRolesAsync(user, newRoles); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } + + public async Task ChangePassword(User user, string currentPassword, string newPassword) + { + try + { + var authenticateResult = await ValidatePassword(user, currentPassword); + + if (!authenticateResult.Success) throw authenticateResult.Exception; + + bool passwordCorrect = authenticateResult.ResultData; + if (!passwordCorrect) return OperationResult.CreateFailure(new InvalidOperationException("Current password incorrect.")); + + await _userManager.ChangePasswordAsync(user, currentPassword, newPassword); + + return OperationResult.CreateSuccessResult(); + } + catch (Exception ex) + { + return OperationResult.CreateFailure(ex); + } + } +} \ No newline at end of file diff --git a/backend/GameDevPortal.WebAPI/appsettings.Development.Template.json b/backend/GameDevPortal.WebAPI/appsettings.Development.Template.json new file mode 100644 index 0000000..ff9bc6a --- /dev/null +++ b/backend/GameDevPortal.WebAPI/appsettings.Development.Template.json @@ -0,0 +1,22 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "ConnectionStrings": { + "ProjectDBConnectionString": "" + }, + "APIConfiguration": { + "MaxPageSize": 20 + }, + "SendGridConfiguration": { + "ApiKey": "" + }, + "JwtSettings": { + "validIssuer": "GameDevPortalAPI", + "validAudience": "openvic2.com" + } +} diff --git a/backend/GameDevPortal.WebAPI/appsettings.json b/backend/GameDevPortal.WebAPI/appsettings.json new file mode 100644 index 0000000..0c208ae --- /dev/null +++ b/backend/GameDevPortal.WebAPI/appsettings.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/backend/GameDevPortal.WebAPI/basicTemplate.txt b/backend/GameDevPortal.WebAPI/basicTemplate.txt new file mode 100644 index 0000000..7cbf8e6 --- /dev/null +++ b/backend/GameDevPortal.WebAPI/basicTemplate.txt @@ -0,0 +1,3 @@ +This is a basic message. +Project title: {Title} +Project description: {Description} \ No newline at end of file diff --git a/backend/GameDevPortal.sln b/backend/GameDevPortal.sln new file mode 100644 index 0000000..26e747d --- /dev/null +++ b/backend/GameDevPortal.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.33414.496 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameDevPortal.WebAPI", "GameDevPortal.WebAPI\GameDevPortal.WebAPI.csproj", "{EBA1C318-BF4C-46F4-86E5-6E24EE498094}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameDevPortal.Core", "GameDevPortal.Core\GameDevPortal.Core.csproj", "{64C0A9AC-CA89-4056-B83D-99D028E41DD1}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GameDevPortal.Infrastructure", "GameDevPortal.Infrastructure\GameDevPortal.Infrastructure.csproj", "{4693C726-63D3-4CAE-ADB4-9B70AEA0C12D}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {EBA1C318-BF4C-46F4-86E5-6E24EE498094}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EBA1C318-BF4C-46F4-86E5-6E24EE498094}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EBA1C318-BF4C-46F4-86E5-6E24EE498094}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EBA1C318-BF4C-46F4-86E5-6E24EE498094}.Release|Any CPU.Build.0 = Release|Any CPU + {64C0A9AC-CA89-4056-B83D-99D028E41DD1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {64C0A9AC-CA89-4056-B83D-99D028E41DD1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {64C0A9AC-CA89-4056-B83D-99D028E41DD1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {64C0A9AC-CA89-4056-B83D-99D028E41DD1}.Release|Any CPU.Build.0 = Release|Any CPU + {4693C726-63D3-4CAE-ADB4-9B70AEA0C12D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4693C726-63D3-4CAE-ADB4-9B70AEA0C12D}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4693C726-63D3-4CAE-ADB4-9B70AEA0C12D}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4693C726-63D3-4CAE-ADB4-9B70AEA0C12D}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {AE42F63D-07A0-43C6-B244-1B4C208A0839} + EndGlobalSection +EndGlobal diff --git a/backend/Utilities/Services/EmailNotificationService.cs b/backend/Utilities/Services/EmailNotificationService.cs new file mode 100644 index 0000000..f41b4ea --- /dev/null +++ b/backend/Utilities/Services/EmailNotificationService.cs @@ -0,0 +1,38 @@ +using GameDevPortal.Core.Interfaces; +using GameDevPortal.Core.Models; +using SendGrid.Helpers.Mail; +using SendGrid; + +namespace Utilities.Services +{ + public class EmailNotificationService : INotificationService + { + async Task INotificationService.Send(Notification notification) + { + string apiKey = Environment.GetEnvironmentVariable("SENDGRID_API_KEY")!; + SendGridClient client = new(apiKey); + EmailAddress from = new("y.rombouts@betabit.nl", "Youri Rombouts"); + string subject = notification.Title; + string plainTextContent = notification.Body; + + List recipientEmailAddresses = new(); + foreach(string recipient in notification.Recipients) + { + recipientEmailAddresses.Add(new EmailAddress(recipient)); + } + + SendGridMessage msg = MailHelper.CreateSingleEmailToMultipleRecipients(from, recipientEmailAddresses, subject, plainTextContent, ""); + + Response response = await client.SendEmailAsync(msg); + + if (response.IsSuccessStatusCode) + { + return OperationResult.CreateSuccessResult(); + } + else + { + return OperationResult.CreateFailure(new Exception(response.Headers.ToString())); + } + } + } +} \ No newline at end of file diff --git a/backend/Utilities/Utilities.csproj b/backend/Utilities/Utilities.csproj new file mode 100644 index 0000000..c58016a --- /dev/null +++ b/backend/Utilities/Utilities.csproj @@ -0,0 +1,17 @@ + + + + net7.0 + enable + enable + + + + + + + + + + +