├── client ├── src │ ├── App.vue │ ├── assets │ │ ├── home │ │ │ ├── images │ │ │ │ ├── send.png │ │ │ │ ├── contact.png │ │ │ │ ├── education.png │ │ │ │ ├── favicon.png │ │ │ │ ├── journey.png │ │ │ │ └── scroll-down.gif │ │ │ ├── fonts │ │ │ │ ├── Mastrih.ttf │ │ │ │ └── Raleway-Regular.ttf │ │ │ └── css │ │ │ │ └── reset.css │ │ └── admin │ │ │ ├── images │ │ │ ├── back.png │ │ │ ├── view.png │ │ │ ├── logout.png │ │ │ ├── messages.png │ │ │ ├── password.png │ │ │ ├── profile.png │ │ │ ├── educations.png │ │ │ ├── default-photo.png │ │ │ ├── experiences.png │ │ │ ├── social-links.png │ │ │ ├── add.svg │ │ │ ├── menu.svg │ │ │ ├── edit.svg │ │ │ └── delete.svg │ │ │ └── css │ │ │ └── login.css │ ├── axiosDefaults.js │ ├── services │ │ ├── profileService.js │ │ ├── identityService.js │ │ ├── socialLinksService.js │ │ ├── educationsService.js │ │ ├── experiencesService.js │ │ └── messagesService.js │ ├── main.js │ ├── components │ │ ├── home │ │ │ ├── HomeFooter.vue │ │ │ ├── Cursor.vue │ │ │ ├── HomeHeader.vue │ │ │ ├── Educations.vue │ │ │ ├── Profile.vue │ │ │ ├── SocialLinks.vue │ │ │ ├── Experiences.vue │ │ │ ├── Loading.vue │ │ │ └── SendMessage.vue │ │ └── admin │ │ │ └── NumberOfUnread.vue │ └── views │ │ ├── home │ │ └── Home.vue │ │ └── admin │ │ ├── messages │ │ ├── ViewMessage.vue │ │ ├── DeleteMessage.vue │ │ └── MessageList.vue │ │ ├── educations │ │ ├── EducationList.vue │ │ └── AddEducation.vue │ │ ├── socialLinks │ │ ├── SocialList.vue │ │ ├── ViewSocial.vue │ │ ├── DeleteSocial.vue │ │ └── AddSocial.vue │ │ ├── experiences │ │ ├── ExperienceList.vue │ │ └── AddExperience.vue │ │ ├── Login.vue │ │ ├── profile │ │ └── ViewProfile.vue │ │ └── ChangePassword.vue ├── nginx.conf ├── .prettierrc.json ├── .dockerignore ├── Dockerfile ├── .eslintrc.cjs ├── index.html ├── vite.config.js └── package.json ├── server ├── src │ ├── WebApi │ │ ├── appsettings.Development.json │ │ ├── appsettings.json │ │ ├── Controllers │ │ │ ├── ProfilesController.cs │ │ │ ├── IdentityController.cs │ │ │ ├── EducationsController.cs │ │ │ ├── ExperiencesController.cs │ │ │ ├── SocialLinksController.cs │ │ │ └── MessagesController.cs │ │ ├── Properties │ │ │ └── launchSettings.json │ │ ├── WebApi.csproj │ │ └── Program.cs │ ├── Domain │ │ ├── Common │ │ │ ├── EntityBase.cs │ │ │ └── AuditableEntityBase.cs │ │ ├── Interfaces │ │ │ ├── IProfileRepository.cs │ │ │ ├── IUserRepository.cs │ │ │ ├── IEducationRepository.cs │ │ │ ├── IExperienceRepository.cs │ │ │ ├── ISocialLinkRepository.cs │ │ │ └── IMessageRepository.cs │ │ ├── Domain.csproj │ │ └── Entities │ │ │ ├── SocialLink.cs │ │ │ ├── User.cs │ │ │ ├── Message.cs │ │ │ ├── Experience.cs │ │ │ ├── Profile.cs │ │ │ └── Education.cs │ ├── Application │ │ ├── Service │ │ │ ├── Interfaces │ │ │ │ ├── IProfileService.cs │ │ │ │ ├── IIdentityService.cs │ │ │ │ ├── IEducationService.cs │ │ │ │ ├── IExperienceService.cs │ │ │ │ ├── ISocialLinkService.cs │ │ │ │ └── IMessageService.cs │ │ │ ├── ProfileService.cs │ │ │ ├── EducationService.cs │ │ │ ├── ExperienceService.cs │ │ │ ├── SocialLinkService.cs │ │ │ ├── MessageService.cs │ │ │ └── IdentityService.cs │ │ ├── DTOs │ │ │ ├── UserLoginDto.cs │ │ │ ├── PasswordDto.cs │ │ │ ├── SocialLinkDto.cs │ │ │ ├── ExperienceDto.cs │ │ │ ├── MessageDto.cs │ │ │ ├── ProfileDto.cs │ │ │ └── EducationDTO.cs │ │ ├── UoW │ │ │ └── IUnitOfWork.cs │ │ ├── DependencyInjection.cs │ │ ├── Application.csproj │ │ └── Extentions │ │ │ └── TimeExtensions.cs │ └── Infrastructure │ │ ├── Configurations │ │ ├── UserConfiguration.cs │ │ ├── SocialLinkConfiguration.cs │ │ ├── MessageConfiguration.cs │ │ ├── ExperienceConfiguration.cs │ │ ├── ProfileConfiguration.cs │ │ └── EducationConfiguration.cs │ │ ├── Repositories │ │ ├── ProfileRepository.cs │ │ ├── UserRepository.cs │ │ ├── SocialLinkRepository.cs │ │ ├── EducationRepository.cs │ │ ├── ExperienceRepository.cs │ │ └── MessageRepository.cs │ │ ├── DependencyInjection.cs │ │ ├── UoW │ │ └── UnitOfWork.cs │ │ ├── Infrastructure.csproj │ │ └── DbContexts │ │ └── PortfolioDbContext.cs ├── tests │ ├── WebApi.Tests.Integration │ │ ├── GlobalUsings.cs │ │ ├── BaseControllerTest.cs │ │ ├── ControllersTests │ │ │ ├── ProfilesControllerTests.cs │ │ │ ├── IdentityControllerTests.cs │ │ │ ├── MessagesControllerTests.cs │ │ │ ├── EducationsControllerTests.cs │ │ │ ├── SocialLinksControllerTests.cs │ │ │ └── ExperiencesControllerTests.cs │ │ ├── WebApi.Tests.Integration.csproj │ │ └── IntegrationTestWebApplicationFactory.cs │ └── Application.Tests.Unit │ │ ├── Usings.cs │ │ ├── Application.Tests.Unit.csproj │ │ ├── ProfileServiceTests.cs │ │ ├── TimeExtensionsTests.cs │ │ ├── MessageServiceTests.cs │ │ ├── EducationServiceTests.cs │ │ ├── ExperienceServiceTests.cs │ │ └── SocialLinkServiceTests.cs ├── Portfolio.lutconfig ├── Dockerfile └── Portfolio.sln ├── LICENSE ├── docker-compose.yml └── README.md /client/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /client/src/assets/home/images/send.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/send.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/back.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/view.png -------------------------------------------------------------------------------- /client/src/assets/home/fonts/Mastrih.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/fonts/Mastrih.ttf -------------------------------------------------------------------------------- /client/src/assets/admin/images/logout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/logout.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/messages.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/messages.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/password.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/password.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/profile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/profile.png -------------------------------------------------------------------------------- /client/src/assets/home/images/contact.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/contact.png -------------------------------------------------------------------------------- /client/src/assets/home/images/education.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/education.png -------------------------------------------------------------------------------- /client/src/assets/home/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/favicon.png -------------------------------------------------------------------------------- /client/src/assets/home/images/journey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/journey.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/educations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/educations.png -------------------------------------------------------------------------------- /client/src/assets/home/images/scroll-down.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/images/scroll-down.gif -------------------------------------------------------------------------------- /client/src/assets/admin/images/default-photo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/default-photo.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/experiences.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/experiences.png -------------------------------------------------------------------------------- /client/src/assets/admin/images/social-links.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/admin/images/social-links.png -------------------------------------------------------------------------------- /client/src/assets/home/fonts/Raleway-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SaraRasoulian/DotNet-WebAPI-Vue-Portfolio/HEAD/client/src/assets/home/fonts/Raleway-Regular.ttf -------------------------------------------------------------------------------- /server/src/WebApi/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft.AspNetCore": "Warning" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /client/nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 8080; 3 | server_name localhost; 4 | root /usr/share/nginx/html; 5 | index index.html index.htm; 6 | location / { 7 | try_files $uri $uri/ /index.html; 8 | } 9 | } -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/GlobalUsings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Application.DTOs; 3 | global using FluentAssertions; 4 | global using System.Net.Http.Json; 5 | global using System.Net; -------------------------------------------------------------------------------- /client/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/prettierrc", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "printWidth": 100, 7 | "trailingComma": "none" 8 | } -------------------------------------------------------------------------------- /server/src/Domain/Common/EntityBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Domain.Common; 4 | 5 | public abstract class EntityBase 6 | { 7 | [Key] 8 | public Guid Id { get; set; } 9 | } -------------------------------------------------------------------------------- /server/Portfolio.lutconfig: -------------------------------------------------------------------------------- 1 | 2 | 3 | true 4 | true 5 | 180000 6 | -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/IProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface IProfileRepository 6 | { 7 | Task Get(); 8 | void Update(Profile model); 9 | } -------------------------------------------------------------------------------- /client/src/axiosDefaults.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | 3 | axios.defaults.baseURL = 'http://localhost:5000' 4 | axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.getItem('token') 5 | 6 | export default axios -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/IProfileService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | 3 | namespace Application.Service.Interfaces; 4 | 5 | public interface IProfileService 6 | { 7 | Task Get(); 8 | Task Update(ProfileDto model); 9 | } -------------------------------------------------------------------------------- /server/src/Domain/Domain.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /server/src/Application/DTOs/UserLoginDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record UserLoginDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string UserName { get; set; } = null!; 8 | 9 | public string Password { get; set; } = null!; 10 | } 11 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/Usings.cs: -------------------------------------------------------------------------------- 1 | global using Xunit; 2 | global using Application.DTOs; 3 | global using Application.Interfaces; 4 | global using Application.Service; 5 | global using Domain.Entities; 6 | global using Domain.Interfaces; 7 | global using FakeItEasy; 8 | global using Mapster; -------------------------------------------------------------------------------- /server/src/Domain/Entities/SocialLink.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class SocialLink : EntityBase 6 | { 7 | public string Name { get; set; } = null!; 8 | 9 | public string URL { get; set; } = null!; 10 | 11 | public string? Icon { get; set; } 12 | } -------------------------------------------------------------------------------- /server/src/Domain/Entities/User.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class User : EntityBase 6 | { 7 | public string UserName { get; set; } = null!; 8 | 9 | public string Password { get; set; } = null!; 10 | 11 | public string? Email { get; set; } 12 | } -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/IUserRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface IUserRepository 6 | { 7 | Task Get(string userName, string password); 8 | Task GetByUserName(string userName); 9 | void Update(User user); 10 | } -------------------------------------------------------------------------------- /server/src/Application/DTOs/PasswordDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record PasswordDto 4 | { 5 | public string currentPassword { get; set; } = null!; 6 | 7 | public string newPassword { get; set; } = null!; 8 | 9 | public string confirmNewPassword { get; set; } = null!; 10 | } 11 | -------------------------------------------------------------------------------- /server/src/Application/DTOs/SocialLinkDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record SocialLinkDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Name { get; set; } = null!; 8 | 9 | public string URL { get; set; } = null!; 10 | 11 | public string? Icon { get; set; } 12 | } 13 | -------------------------------------------------------------------------------- /client/src/services/profileService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/profiles' 4 | 5 | const profileService = { 6 | async get() { 7 | return await axios.get(`${path}`) 8 | }, 9 | async update(model) { 10 | return await axios.put(`${path}`, model) 11 | } 12 | } 13 | 14 | export default profileService -------------------------------------------------------------------------------- /client/.dockerignore: -------------------------------------------------------------------------------- 1 | # Items that don't need to be in a Docker image. 2 | # Anything not used by the build system should go here. 3 | .git 4 | .dockerignore 5 | .gitignore 6 | .github/* 7 | README.md 8 | Dockerfile 9 | 10 | # Artifacts that will be built during image creation. 11 | # This should contain all files created during `yarn build`. 12 | dist 13 | node_modules -------------------------------------------------------------------------------- /client/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-alpine as build-stage 2 | WORKDIR /app 3 | COPY package*.json ./ 4 | RUN npm install 5 | COPY . . 6 | RUN npm run build 7 | 8 | FROM nginx:stable-alpine as production-stage 9 | COPY --from=build-stage /app/dist /usr/share/nginx/html 10 | COPY nginx.conf /etc/nginx/conf.d/default.conf 11 | 12 | EXPOSE 8080 13 | CMD ["nginx", "-g", "daemon off;"] -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/IIdentityService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Domain.Entities; 3 | 4 | namespace Application.Service.Interfaces; 5 | 6 | public interface IIdentityService 7 | { 8 | Task Login(UserLoginDto userLogin); 9 | Task ChangePassword(PasswordDto model, string userName); 10 | string GenerateToken(User user); 11 | } -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/IEducationRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface IEducationRepository 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(Education model); 10 | void Update(Education model); 11 | void Delete(Education model); 12 | } -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/IExperienceRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface IExperienceRepository 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(Experience model); 10 | void Update(Experience model); 11 | void Delete(Experience model); 12 | } -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/ISocialLinkRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface ISocialLinkRepository 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(SocialLink model); 10 | void Update(SocialLink model); 11 | void Delete(SocialLink model); 12 | } -------------------------------------------------------------------------------- /client/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import router from './router' 4 | import axios from 'axios' 5 | import VueAxios from 'vue-axios' 6 | import Toast from 'vue-toastification' 7 | import 'vue-toastification/dist/index.css' 8 | 9 | const app = createApp(App) 10 | app.use(router) 11 | app.use(VueAxios, axios) 12 | app.use(Toast) 13 | app.mount('#app') -------------------------------------------------------------------------------- /client/src/components/home/HomeFooter.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/src/Domain/Interfaces/IMessageRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | 3 | namespace Domain.Interfaces; 4 | 5 | public interface IMessageRepository 6 | { 7 | Task> GetAll(); 8 | Task GetNumberOfUnread(); 9 | Task GetById(Guid id); 10 | Task Add(Message model); 11 | void Update(Message model); 12 | void Delete(Message model); 13 | } -------------------------------------------------------------------------------- /server/src/Domain/Entities/Message.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class Message : EntityBase 6 | { 7 | public string Name { get; set; } = null!; 8 | 9 | public string Email { get; set; } = null!; 10 | 11 | public string Content { get; set; } = null!; 12 | 13 | public DateTime SentAt { get; set; } 14 | 15 | public bool IsRead { get; set; } 16 | } -------------------------------------------------------------------------------- /client/src/assets/admin/images/add.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /server/src/Domain/Common/AuditableEntityBase.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Domain.Common; 4 | 5 | public abstract class AuditableEntityBase 6 | { 7 | [Key] 8 | public Guid Id { get; set; } 9 | public DateTime CreatedAt { get; set; } 10 | public Guid CreatedBy { get; set; } 11 | public DateTime LastUpdatedAt { get; set; } 12 | public Guid LastUpdatedBy { get; set; } 13 | } -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | require('@rushstack/eslint-patch/modern-module-resolution') 3 | 4 | module.exports = { 5 | root: true, 6 | 'extends': [ 7 | 'plugin:vue/vue3-essential', 8 | 'eslint:recommended', 9 | '@vue/eslint-config-prettier/skip-formatting' 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 'latest' 13 | }, 14 | "rules": {"vue/multi-word-component-names": "off"} 15 | } 16 | -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/IEducationService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | 3 | namespace Application.Service.Interfaces; 4 | 5 | public interface IEducationService 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(EducationDto model); 10 | Task Update(Guid id, EducationDto model); 11 | Task Delete(Guid id); 12 | } 13 | -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/IExperienceService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | 3 | namespace Application.Service.Interfaces; 4 | 5 | public interface IExperienceService 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(ExperienceDto model); 10 | Task Update(Guid id, ExperienceDto model); 11 | Task Delete(Guid id); 12 | } 13 | -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/ISocialLinkService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | 3 | namespace Application.Service.Interfaces; 4 | 5 | public interface ISocialLinkService 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(SocialLinkDto model); 10 | Task Update(Guid id, SocialLinkDto model); 11 | Task Delete(Guid id); 12 | } 13 | -------------------------------------------------------------------------------- /server/src/Application/Service/Interfaces/IMessageService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | 3 | namespace Application.Service.Interfaces; 4 | 5 | public interface IMessageService 6 | { 7 | Task> GetAll(); 8 | Task GetById(Guid id); 9 | Task Add(MessageDto model); 10 | Task Delete(Guid id); 11 | Task GetNumberOfUnread(); 12 | Task MarkAsRead(Guid id); 13 | } 14 | -------------------------------------------------------------------------------- /server/src/Domain/Entities/Experience.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class Experience : EntityBase 6 | { 7 | public string CompanyName { get; set; } = null!; 8 | 9 | public string StartYear { get; set; } = null!; 10 | 11 | public string EndYear { get; set; } = null!; 12 | 13 | public string Description { get; set; } = null!; 14 | 15 | public string? Website { get; set; } 16 | } -------------------------------------------------------------------------------- /server/src/Application/DTOs/ExperienceDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record ExperienceDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string CompanyName { get; set; } = null!; 8 | 9 | public string StartYear { get; set; } = null!; 10 | 11 | public string EndYear { get; set; } = null!; 12 | 13 | public string? Description { get; set; } 14 | 15 | public string? Website { get; set; } = string.Empty; 16 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Portfolio | Sara Rasoulian 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | 3 | import { defineConfig } from 'vite' 4 | import vue from '@vitejs/plugin-vue' 5 | import vueJsx from '@vitejs/plugin-vue-jsx' 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig({ 9 | plugins: [ 10 | vue(), 11 | vueJsx(), 12 | ], 13 | resolve: { 14 | alias: { 15 | '@': fileURLToPath(new URL('./src', import.meta.url)) 16 | } 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /server/src/Application/DTOs/MessageDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record MessageDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Name { get; set; } = null!; 8 | 9 | public string Email { get; set; } = null!; 10 | 11 | public string Content { get; set; } = null!; 12 | 13 | public bool IsRead { get; set; } 14 | 15 | public string? SentAt { get; set; } 16 | 17 | public string? TimeAgo { get; set; } 18 | } -------------------------------------------------------------------------------- /server/src/Domain/Entities/Profile.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class Profile : EntityBase 6 | { 7 | public string FirstName { get; set; } = null!; 8 | 9 | public string LastName { get; set; } = null!; 10 | 11 | public string Email { get; set; } = null!; 12 | 13 | public string Headline { get; set; } = null!; 14 | 15 | public string About { get; set; } = null!; 16 | 17 | public string? Photo { get; set; } 18 | } -------------------------------------------------------------------------------- /server/src/Domain/Entities/Education.cs: -------------------------------------------------------------------------------- 1 | using Domain.Common; 2 | 3 | namespace Domain.Entities; 4 | 5 | public class Education : EntityBase 6 | { 7 | public string Degree { get; set; } = null!; 8 | 9 | public string FieldOfStudy { get; set; } = null!; 10 | 11 | public string StartYear { get; set; } = null!; 12 | 13 | public string EndYear { get; set; } = null!; 14 | 15 | public string? School { get; set; } 16 | 17 | public string? Description { get; set; } 18 | } -------------------------------------------------------------------------------- /server/src/Application/DTOs/ProfileDto.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record ProfileDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string FirstName { get; set; } = null!; 8 | 9 | public string LastName { get; set; } = null!; 10 | 11 | public string Email { get; set; } = null!; 12 | 13 | public string Headline { get; set; } = null!; 14 | 15 | public string About { get; set; } = null!; 16 | 17 | public string? Photo { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/services/identityService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/identity' 4 | 5 | const identityService = { 6 | async login(userLogin) { 7 | return await axios.post(`${path}/login`, userLogin) 8 | }, 9 | async validateToken() { 10 | return await axios.get(`${path}/validate-token`) 11 | }, 12 | async changePassword(model) { 13 | return await axios.put(`${path}/change-password`, model) 14 | } 15 | } 16 | 17 | export default identityService 18 | -------------------------------------------------------------------------------- /server/src/Application/DTOs/EducationDTO.cs: -------------------------------------------------------------------------------- 1 | namespace Application.DTOs; 2 | 3 | public record EducationDto 4 | { 5 | public Guid Id { get; set; } 6 | 7 | public string Degree { get; set; } = null!; 8 | 9 | public string FieldOfStudy { get; set; } = null!; 10 | 11 | public string StartYear { get; set; } = null!; 12 | 13 | public string EndYear { get; set; } = null!; 14 | 15 | public string? School { get; set; } 16 | 17 | public string? Description { get; set; } 18 | } 19 | -------------------------------------------------------------------------------- /server/src/Application/UoW/IUnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Domain.Interfaces; 2 | 3 | namespace Application.Interfaces; 4 | 5 | public interface IUnitOfWork : IDisposable 6 | { 7 | IProfileRepository Profile { get; } 8 | IEducationRepository Education { get; } 9 | IExperienceRepository Experience { get; } 10 | IMessageRepository Message { get; } 11 | ISocialLinkRepository SocialLink { get; } 12 | IUserRepository User { get; } 13 | 14 | Task SaveChangesAsync(CancellationToken cancellationToken = default); 15 | } -------------------------------------------------------------------------------- /client/src/components/admin/NumberOfUnread.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /client/src/assets/admin/images/menu.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/UserConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class UserConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.UserName) 12 | .HasMaxLength(50) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.Password) 16 | .IsRequired(); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /client/src/services/socialLinksService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/social-links' 4 | 5 | const socialLinksService = { 6 | async getAll() { 7 | return await axios.get(`${path}`) 8 | }, 9 | async get(id) { 10 | return await axios.get(`${path}/${id}`) 11 | }, 12 | async update(id, model) { 13 | return await axios.put(`${path}/${id}`, model) 14 | }, 15 | async create(model) { 16 | return await axios.post(`${path}`, model) 17 | }, 18 | async delete(id) { 19 | return await axios.delete(`${path}/${id}`) 20 | } 21 | } 22 | 23 | export default socialLinksService -------------------------------------------------------------------------------- /client/src/services/educationsService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/educations' 4 | 5 | const educationsService = { 6 | async getAll() { 7 | return await axios.get(`${path}`) 8 | }, 9 | async get(id) { 10 | return await axios.get(`${path}/${id}`) 11 | }, 12 | async update(id, model) { 13 | return await axios.put(`${path}/${id}`, model) 14 | }, 15 | async create(model) { 16 | return await axios.post(`${path}`, model) 17 | }, 18 | async delete(id) { 19 | return await axios.delete(`${path}/${id}`) 20 | } 21 | } 22 | 23 | export default educationsService 24 | -------------------------------------------------------------------------------- /client/src/services/experiencesService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/experiences' 4 | 5 | const experiencesService = { 6 | async getAll() { 7 | return await axios.get(`${path}`) 8 | }, 9 | async get(id) { 10 | return await axios.get(`${path}/${id}`) 11 | }, 12 | async update(id, model) { 13 | return await axios.put(`${path}/${id}`, model) 14 | }, 15 | async create(model) { 16 | return await axios.post(`${path}`, model) 17 | }, 18 | async delete(id) { 19 | return await axios.delete(`${path}/${id}`) 20 | } 21 | } 22 | 23 | export default experiencesService 24 | -------------------------------------------------------------------------------- /server/src/WebApi/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | // Connect to the database in the Docker container 4 | "DefaultConnection": "Host=localhost;Port=5433;Database=PortfolioDB;Username=sara;Password=mysecretpassword" 5 | }, 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Information", 9 | "Microsoft.AspNetCore": "Warning" 10 | } 11 | }, 12 | "JwtSettings": { 13 | "Issuer": "http://localhost:5000/", 14 | "Audience": "http://localhost:5000/", 15 | "Key": "This is a sample secret key - please don't use in production environment" 16 | }, 17 | "AllowedHosts": "*" 18 | } 19 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/SocialLinkConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class SocialLinkConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.Name) 12 | .HasMaxLength(50) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.URL) 16 | .HasMaxLength(2000) 17 | .IsRequired(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /client/src/services/messagesService.js: -------------------------------------------------------------------------------- 1 | import axios from '@/axiosDefaults' 2 | 3 | const path = '/api/messages' 4 | 5 | const messagesService = { 6 | async getAll() { 7 | return await axios.get(`${path}`) 8 | }, 9 | async get(id) { 10 | return await axios.get(`${path}/${id}`) 11 | }, 12 | async create(model) { 13 | return await axios.post(`${path}`, model) 14 | }, 15 | async delete(id) { 16 | return await axios.delete(`${path}/${id}`) 17 | }, 18 | async GetNumberOfUnread() { 19 | return await axios.get(`${path}/unread`) 20 | }, 21 | async markAsRead(id) { 22 | return await axios.put(`${path}/mark-as-read/${id}`) 23 | }, 24 | } 25 | 26 | export default messagesService -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/ProfileRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class ProfileRepository : IProfileRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public ProfileRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task Get() 17 | { 18 | return await _dbContext.Profiles.AsNoTracking().FirstOrDefaultAsync(); 19 | } 20 | 21 | public void Update(Profile model) 22 | { 23 | _dbContext.Profiles.Update(model); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/dotnet/aspnet:7.0 AS base 2 | WORKDIR /app 3 | EXPOSE 5000 4 | 5 | FROM mcr.microsoft.com/dotnet/sdk:7.0 AS build 6 | WORKDIR /src 7 | COPY ["src/WebApi/WebApi.csproj", "src/WebApi/"] 8 | COPY ["src/Application/Application.csproj", "src/Application/"] 9 | COPY ["src/Domain/Domain.csproj", "src/Domain/"] 10 | COPY ["src/Infrastructure/Infrastructure.csproj", "src/Infrastructure/"] 11 | RUN dotnet restore "./src/WebApi/WebApi.csproj" 12 | COPY . . 13 | WORKDIR "/src/src/WebApi" 14 | RUN dotnet build "WebApi.csproj" -c Release -o /app/build 15 | 16 | FROM build AS publish 17 | RUN dotnet publish "WebApi.csproj" -c Release -o /app/publish 18 | 19 | FROM base AS final 20 | WORKDIR /app 21 | 22 | COPY --from=publish /app/publish . 23 | ENTRYPOINT ["dotnet", "WebApi.dll"] -------------------------------------------------------------------------------- /server/src/Application/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Application.Service; 2 | using Application.Service.Interfaces; 3 | 4 | namespace Microsoft.Extensions.DependencyInjection; 5 | 6 | public static class DependencyInjection 7 | { 8 | public static IServiceCollection AddApplicationServices(this IServiceCollection services) 9 | { 10 | services.AddScoped(); 11 | services.AddScoped(); 12 | services.AddScoped(); 13 | services.AddScoped(); 14 | services.AddScoped(); 15 | services.AddScoped(); 16 | 17 | return services; 18 | } 19 | } -------------------------------------------------------------------------------- /client/src/assets/admin/css/login.css: -------------------------------------------------------------------------------- 1 | .login-container { 2 | margin: 80px auto; 3 | width: 32rem; 4 | } 5 | 6 | .login-buttons { 7 | display: grid; 8 | text-align: center; 9 | } 10 | 11 | .btn-login { 12 | background-color: var(--gray); 13 | color: var(--text-color); 14 | width: 100%; 15 | margin-bottom: 5px; 16 | } 17 | 18 | .btn-login:hover, 19 | .btn-login:focus { 20 | background-color: var(--dark-gray); 21 | color: var(--text-color); 22 | } 23 | 24 | .back-home { 25 | text-decoration: none; 26 | color: var(--text-color); 27 | } 28 | 29 | .back-home:hover { 30 | color: black; 31 | } 32 | 33 | .login-title { 34 | margin: 0 auto; 35 | } 36 | 37 | @media (max-width: 426px) { 38 | .login-container { 39 | margin: 10px; 40 | width: auto; 41 | padding-top: 30px; 42 | } 43 | } -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/MessageConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class MessageConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.Name) 12 | .HasMaxLength(100) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.Email) 16 | .HasMaxLength(320) 17 | .IsRequired(); 18 | 19 | builder.Property(t => t.Content) 20 | .HasMaxLength(1000) 21 | .IsRequired(); 22 | 23 | builder.Property(t => t.IsRead) 24 | .IsRequired(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /server/src/Application/Application.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/src/Infrastructure/DependencyInjection.cs: -------------------------------------------------------------------------------- 1 | using Application.Interfaces; 2 | using Infrastructure.DbContexts; 3 | using Infrastructure.UoW; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.Extensions.Configuration; 6 | 7 | namespace Microsoft.Extensions.DependencyInjection; 8 | 9 | public static class DependencyInjection 10 | { 11 | public static IServiceCollection AddInfrastructureServices(this IServiceCollection services, IConfiguration configuration) 12 | { 13 | // Setting DBContexts 14 | var connectionString = configuration.GetConnectionString("DefaultConnection"); 15 | services.AddDbContext(options => options.UseNpgsql(connectionString, o => o.UseNodaTime())); 16 | services.AddHealthChecks().AddNpgSql(connectionString, "PortfolioDB"); 17 | 18 | services.AddScoped(); 19 | 20 | return services; 21 | } 22 | } -------------------------------------------------------------------------------- /client/src/components/home/Cursor.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | -------------------------------------------------------------------------------- /client/src/components/home/HomeHeader.vue: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/ExperienceConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class ExperienceConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.CompanyName) 12 | .HasMaxLength(100) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.StartYear) 16 | .HasMaxLength(10) 17 | .IsRequired(); 18 | 19 | builder.Property(t => t.EndYear) 20 | .HasMaxLength(10) 21 | .IsRequired(); 22 | 23 | builder.Property(t => t.Description) 24 | .HasMaxLength(1000) 25 | .IsRequired(); 26 | 27 | builder.Property(t => t.Website) 28 | .HasMaxLength(255); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/ProfileConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class ProfileConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.FirstName) 12 | .HasMaxLength(50) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.LastName) 16 | .HasMaxLength(50) 17 | .IsRequired(); 18 | 19 | builder.Property(t => t.Email) 20 | .HasMaxLength(320) 21 | .IsRequired(); 22 | 23 | builder.Property(t => t.Headline) 24 | .HasMaxLength(100) 25 | .IsRequired(); 26 | 27 | builder.Property(t => t.About) 28 | .HasMaxLength(1000) 29 | .IsRequired(); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/UserRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class UserRepository : IUserRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public UserRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task Get(string userName, string password) 17 | { 18 | return await _dbContext.Users 19 | .FirstOrDefaultAsync(c => c.UserName.ToLower() == userName.ToLower() && c.Password == password); 20 | } 21 | 22 | public async Task GetByUserName(string userName) 23 | { 24 | return await _dbContext.Users.FirstOrDefaultAsync(c => c.UserName.ToLower() == userName.ToLower()); 25 | } 26 | 27 | 28 | public async void Update(User user) 29 | { 30 | _dbContext.Users.Update(user); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite --host", 7 | "build": "vite build", 8 | "preview": "vite preview", 9 | "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", 10 | "format": "prettier --write src/" 11 | }, 12 | "dependencies": { 13 | "@vuelidate/core": "^2.0.3", 14 | "@vuelidate/validators": "^2.0.4", 15 | "axios": "^1.6.0", 16 | "bootstrap": "^5.3.1", 17 | "jquery": "^3.7.1", 18 | "popper.js": "^1.16.1", 19 | "vue": "^3.3.4", 20 | "vue-axios": "^3.5.2", 21 | "vue-router": "^4.2.4", 22 | "vue-toastification": "^2.0.0-rc.5" 23 | }, 24 | "devDependencies": { 25 | "@rushstack/eslint-patch": "^1.3.2", 26 | "@vitejs/plugin-vue": "^4.3.1", 27 | "@vitejs/plugin-vue-jsx": "^3.0.2", 28 | "@vue/eslint-config-prettier": "^8.0.0", 29 | "eslint": "^8.46.0", 30 | "eslint-plugin-vue": "^9.16.1", 31 | "prettier": "^3.0.0", 32 | "vite": "^4.5.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Configurations/EducationConfiguration.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using Microsoft.EntityFrameworkCore.Metadata.Builders; 4 | 5 | namespace Infrastructure.Configurations; 6 | 7 | public class EducationConfiguration : IEntityTypeConfiguration 8 | { 9 | public void Configure(EntityTypeBuilder builder) 10 | { 11 | builder.Property(t => t.Degree) 12 | .HasMaxLength(50) 13 | .IsRequired(); 14 | 15 | builder.Property(t => t.FieldOfStudy) 16 | .HasMaxLength(250) 17 | .IsRequired(); 18 | 19 | builder.Property(t => t.StartYear) 20 | .HasMaxLength(10) 21 | .IsRequired(); 22 | 23 | builder.Property(t => t.EndYear) 24 | .HasMaxLength(10) 25 | .IsRequired(); 26 | 27 | builder.Property(t => t.School) 28 | .HasMaxLength(250); 29 | 30 | builder.Property(t => t.Description) 31 | .HasMaxLength(1000); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/ProfilesController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Security.Claims; 6 | 7 | namespace WebApi.Controllers; 8 | 9 | [Route("api/profiles")] 10 | [ApiController] 11 | public class ProfilesController : ControllerBase 12 | { 13 | private readonly IProfileService _ProfileService; 14 | public ProfilesController(IProfileService ProfileService) 15 | { 16 | _ProfileService = ProfileService; 17 | } 18 | 19 | [HttpGet] 20 | public async Task Get() 21 | { 22 | var result = await _ProfileService.Get(); 23 | return Ok(result); 24 | } 25 | 26 | [HttpPut] 27 | [Authorize] 28 | public async Task Put([FromBody] ProfileDto dto) 29 | { 30 | if (!ModelState.IsValid) return BadRequest(ModelState); 31 | var result = await _ProfileService.Update(dto); 32 | if (!result) return BadRequest(); 33 | return Ok(); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /server/src/Application/Service/ProfileService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Interfaces; 3 | using Application.Service.Interfaces; 4 | using Domain.Entities; 5 | using Mapster; 6 | 7 | namespace Application.Service; 8 | 9 | public class ProfileService : IProfileService 10 | { 11 | private readonly IUnitOfWork _unitOfWork; 12 | public ProfileService(IUnitOfWork unitOfWork) 13 | { 14 | _unitOfWork = unitOfWork; 15 | } 16 | 17 | public async Task Get() 18 | { 19 | var result = await _unitOfWork.Profile.Get(); 20 | return result?.Adapt(); 21 | } 22 | 23 | public async Task Update(ProfileDto model) 24 | { 25 | if (model is null) return false; 26 | 27 | var toUpdate = await _unitOfWork.Profile.Get(); 28 | if (toUpdate is null) return false; 29 | 30 | if (toUpdate.Id != model.Id) return false; 31 | 32 | toUpdate = model.Adapt(); 33 | 34 | _unitOfWork.Profile.Update(toUpdate); 35 | await _unitOfWork.SaveChangesAsync(); 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/BaseControllerTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Headers; 2 | 3 | namespace WebApi.Tests.Integration; 4 | 5 | public class BaseControllerTest : IClassFixture 6 | { 7 | protected readonly HttpClient _httpClient; 8 | 9 | protected BaseControllerTest(IntegrationTestWebApplicationFactory factory) 10 | { 11 | _httpClient = factory.CreateDefaultClient(); 12 | } 13 | 14 | /// 15 | /// Authenticate using the one user created by database seed 16 | /// 17 | /// 18 | protected async Task AuthenticateAsync() 19 | { 20 | UserLoginDto userLoginDto = new UserLoginDto 21 | { 22 | UserName = "admin", 23 | Password = "123456" 24 | }; 25 | 26 | var loginResponse = await _httpClient.PostAsJsonAsync("api/identity/login", userLoginDto); 27 | string token = await loginResponse.Content.ReadAsStringAsync(); 28 | 29 | _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); 30 | } 31 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Sara Rasoulian 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /client/src/components/home/Educations.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /client/src/components/home/Profile.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 39 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/SocialLinkRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class SocialLinkRepository : ISocialLinkRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public SocialLinkRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task> GetAll() 17 | { 18 | return await _dbContext.SocialLinks.ToListAsync(); 19 | } 20 | 21 | public async Task GetById(Guid id) 22 | { 23 | return await _dbContext.SocialLinks.AsNoTracking().FirstOrDefaultAsync(c=>c.Id == id); 24 | } 25 | 26 | public async Task Add(SocialLink model) 27 | { 28 | var result = await _dbContext.SocialLinks.AddAsync(model); 29 | return result.Entity; 30 | } 31 | 32 | public void Update(SocialLink model) 33 | { 34 | _dbContext.SocialLinks.Update(model); 35 | } 36 | 37 | public void Delete(SocialLink model) 38 | { 39 | _dbContext.SocialLinks.Remove(model); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/WebApi/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/launchsettings.json", 3 | "iisSettings": { 4 | "windowsAuthentication": false, 5 | "anonymousAuthentication": true, 6 | "iisExpress": { 7 | "applicationUrl": "http://localhost:47799", 8 | "sslPort": 44369 9 | } 10 | }, 11 | "profiles": { 12 | "http": { 13 | "commandName": "Project", 14 | "dotnetRunMessages": true, 15 | "launchBrowser": true, 16 | "launchUrl": "swagger", 17 | "applicationUrl": "http://localhost:5135", 18 | "environmentVariables": { 19 | "ASPNETCORE_ENVIRONMENT": "Development" 20 | } 21 | }, 22 | "https": { 23 | "commandName": "Project", 24 | "dotnetRunMessages": true, 25 | "launchBrowser": true, 26 | "launchUrl": "swagger", 27 | "applicationUrl": "https://localhost:7026;http://localhost:5135", 28 | "environmentVariables": { 29 | "ASPNETCORE_ENVIRONMENT": "Development" 30 | } 31 | }, 32 | "IIS Express": { 33 | "commandName": "IISExpress", 34 | "launchBrowser": true, 35 | "launchUrl": "swagger", 36 | "environmentVariables": { 37 | "ASPNETCORE_ENVIRONMENT": "Development" 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/EducationRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class EducationRepository : IEducationRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public EducationRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task> GetAll() 17 | { 18 | return await _dbContext.Educations.OrderByDescending(c => c.StartYear).ThenByDescending(c => c.EndYear).ToListAsync(); 19 | } 20 | 21 | public async Task GetById(Guid id) 22 | { 23 | return await _dbContext.Educations.AsNoTracking().FirstOrDefaultAsync(c=>c.Id == id); 24 | } 25 | 26 | public async Task Add(Education model) 27 | { 28 | var result = await _dbContext.Educations.AddAsync(model); 29 | return result.Entity; 30 | } 31 | 32 | public void Update(Education model) 33 | { 34 | _dbContext.Educations.Update(model); 35 | } 36 | 37 | public void Delete(Education model) 38 | { 39 | _dbContext.Educations.Remove(model); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/ExperienceRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class ExperienceRepository : IExperienceRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public ExperienceRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task> GetAll() 17 | { 18 | return await _dbContext.Experiences.OrderByDescending(c => c.StartYear).ThenByDescending(c => c.EndYear).ToListAsync(); 19 | } 20 | 21 | public async Task GetById(Guid id) 22 | { 23 | return await _dbContext.Experiences.AsNoTracking().FirstOrDefaultAsync(c=>c.Id == id); 24 | } 25 | 26 | public async Task Add(Experience model) 27 | { 28 | var result = await _dbContext.Experiences.AddAsync(model); 29 | return result.Entity; 30 | } 31 | 32 | public void Update(Experience model) 33 | { 34 | _dbContext.Experiences.Update(model); 35 | } 36 | 37 | public void Delete(Experience model) 38 | { 39 | _dbContext.Experiences.Remove(model); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/src/components/home/SocialLinks.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/Application.Tests.Unit.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /client/src/assets/home/css/reset.css: -------------------------------------------------------------------------------- 1 | *:where(:not(html, iframe, canvas, img, svg, video, audio):not(svg *, symbol *)) { 2 | all: unset; 3 | display: revert; 4 | } 5 | 6 | *, 7 | *::before, 8 | *::after { 9 | box-sizing: border-box; 10 | } 11 | 12 | a, button { 13 | cursor: revert; 14 | } 15 | 16 | ol, ul, menu { 17 | list-style: none; 18 | } 19 | 20 | img { 21 | max-inline-size: 100%; 22 | max-block-size: 100%; 23 | } 24 | 25 | table { 26 | border-collapse: collapse; 27 | } 28 | 29 | input, textarea { 30 | -webkit-user-select: auto; 31 | } 32 | 33 | textarea { 34 | white-space: revert; 35 | } 36 | 37 | meter { 38 | -webkit-appearance: revert; 39 | appearance: revert; 40 | } 41 | 42 | :where(pre) { 43 | all: revert; 44 | } 45 | 46 | ::placeholder { 47 | color: unset; 48 | } 49 | 50 | ::marker { 51 | content: initial; 52 | } 53 | 54 | :where([hidden]) { 55 | display: none; 56 | } 57 | 58 | :where([contenteditable]:not([contenteditable="false"])) { 59 | -moz-user-modify: read-write; 60 | -webkit-user-modify: read-write; 61 | overflow-wrap: break-word; 62 | -webkit-line-break: after-white-space; 63 | -webkit-user-select: auto; 64 | } 65 | 66 | :where([draggable="true"]) { 67 | -webkit-user-drag: element; 68 | } 69 | 70 | :where(dialog:modal) { 71 | all: revert; 72 | } -------------------------------------------------------------------------------- /server/src/Infrastructure/Repositories/MessageRepository.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Domain.Interfaces; 3 | using Infrastructure.DbContexts; 4 | using Microsoft.EntityFrameworkCore; 5 | 6 | namespace Infrastructure.Repositories; 7 | 8 | public class MessageRepository : IMessageRepository 9 | { 10 | private readonly PortfolioDbContext _dbContext; 11 | public MessageRepository(PortfolioDbContext dbContext) 12 | { 13 | _dbContext = dbContext; 14 | } 15 | 16 | public async Task> GetAll() 17 | { 18 | return await _dbContext.Messages.OrderBy(c=>c.IsRead).ThenByDescending(c => c.SentAt).ToListAsync(); 19 | } 20 | 21 | public async Task GetNumberOfUnread() 22 | { 23 | return await _dbContext.Messages.Where(c=> !c.IsRead).CountAsync(); 24 | } 25 | 26 | public async Task GetById(Guid id) 27 | { 28 | return await _dbContext.Messages.AsNoTracking().FirstOrDefaultAsync(c => c.Id == id); 29 | } 30 | 31 | public async Task Add(Message model) 32 | { 33 | var result = await _dbContext.Messages.AddAsync(model); 34 | return result.Entity; 35 | } 36 | 37 | public void Delete(Message model) 38 | { 39 | _dbContext.Messages.Remove(model); 40 | } 41 | 42 | public void Update(Message model) 43 | { 44 | _dbContext.Messages.Update(model); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /client/src/components/home/Experiences.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/ProfilesControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class ProfilesControllerTests : BaseControllerTest 4 | { 5 | public ProfilesControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task Get_Returns_OK_With_ProfileDto() 10 | { 11 | // Act 12 | var response = await _httpClient.GetAsync("api/profiles"); 13 | 14 | // Assert 15 | response.EnsureSuccessStatusCode(); 16 | 17 | // Should return the one profile created by database seed 18 | var profile = await response.Content.ReadFromJsonAsync(); 19 | profile.Should().NotBeNull(); 20 | } 21 | 22 | [Fact] 23 | public async void Put_Returns_SuccessStatusCode() 24 | { 25 | // Arrange 26 | await AuthenticateAsync(); 27 | 28 | // Get the one profile created by database seed 29 | var response = await _httpClient.GetAsync("api/profiles"); 30 | ProfileDto profileDto = await response.Content.ReadFromJsonAsync(); 31 | 32 | // Update the profile 33 | profileDto.FirstName = "Emma"; 34 | 35 | // Act 36 | var updateResponse = await _httpClient.PutAsJsonAsync("/api/profiles/", profileDto); 37 | 38 | // Assert 39 | updateResponse.EnsureSuccessStatusCode(); 40 | } 41 | } -------------------------------------------------------------------------------- /client/src/assets/admin/images/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /server/src/Infrastructure/UoW/UnitOfWork.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.DbContexts; 2 | using Application.Interfaces; 3 | using Domain.Interfaces; 4 | using Infrastructure.Repositories; 5 | 6 | namespace Infrastructure.UoW; 7 | 8 | public sealed class UnitOfWork : IUnitOfWork 9 | { 10 | public IProfileRepository Profile { get; private set; } 11 | public IEducationRepository Education { get; private set; } 12 | public IExperienceRepository Experience { get; private set; } 13 | public IMessageRepository Message { get; private set; } 14 | public ISocialLinkRepository SocialLink { get; private set; } 15 | public IUserRepository User { get; private set; } 16 | 17 | private readonly PortfolioDbContext _dbContext; 18 | public UnitOfWork(PortfolioDbContext dbContext) 19 | { 20 | _dbContext = dbContext; 21 | Profile = new ProfileRepository(_dbContext); 22 | Education = new EducationRepository(_dbContext); 23 | Experience = new ExperienceRepository(_dbContext); 24 | Message = new MessageRepository(_dbContext); 25 | SocialLink = new SocialLinkRepository(_dbContext); 26 | User = new UserRepository(_dbContext); 27 | } 28 | 29 | public async Task SaveChangesAsync(CancellationToken cancellationToken = default) 30 | { 31 | return await _dbContext.SaveChangesAsync(cancellationToken); 32 | } 33 | 34 | public void Dispose() 35 | { 36 | _dbContext.DisposeAsync(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/WebApi.Tests.Integration.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | all 21 | 22 | 23 | runtime; build; native; contentfiles; analyzers; buildtransitive 24 | all 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /server/src/Infrastructure/Infrastructure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | all 15 | runtime; build; native; contentfiles; analyzers; buildtransitive 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/IdentityController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | using System.Security.Claims; 6 | 7 | namespace WebApi.Controllers; 8 | 9 | [Route("api/identity")] 10 | [ApiController] 11 | public class IdentityController : ControllerBase 12 | { 13 | private readonly IIdentityService _identityService; 14 | public IdentityController(IIdentityService identityService) 15 | { 16 | _identityService = identityService; 17 | } 18 | 19 | [HttpPost("login")] 20 | public async Task Login([FromBody] UserLoginDto dto) 21 | { 22 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 23 | var result = await _identityService.Login(dto); 24 | 25 | if (result is null) return NoContent(); 26 | return Ok(result); 27 | } 28 | 29 | [HttpPut("change-password")] 30 | [Authorize] 31 | public async Task ChangePassword([FromBody] PasswordDto dto) 32 | { 33 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 34 | 35 | string userName = User.FindFirst(ClaimTypes.NameIdentifier)?.Value; 36 | 37 | var result = await _identityService.ChangePassword(dto, userName); 38 | if (!result) return BadRequest(); 39 | return Ok(); 40 | } 41 | 42 | [HttpGet("validate-token")] 43 | [Authorize] 44 | public IActionResult ValidateToken() 45 | { 46 | // If the request reaches this point, it means the token is valid 47 | return Ok(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /server/src/Application/Extentions/TimeExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Extentions; 2 | 3 | public static class TimeExtensions 4 | { 5 | public static string TimeAgo(this DateTime dateTime) 6 | { 7 | string result = string.Empty; 8 | var timeSpan = DateTime.UtcNow.Subtract(dateTime); 9 | 10 | if (timeSpan <= TimeSpan.FromSeconds(60)) 11 | { 12 | result = string.Format("{0} seconds ago", timeSpan.Seconds); 13 | } 14 | else if (timeSpan <= TimeSpan.FromMinutes(60)) 15 | { 16 | result = timeSpan.Minutes > 1 ? 17 | String.Format("{0} minutes ago", timeSpan.Minutes) : 18 | "about a minute ago"; 19 | } 20 | else if (timeSpan <= TimeSpan.FromHours(24)) 21 | { 22 | result = timeSpan.Hours > 1 ? 23 | String.Format("{0} hours ago", timeSpan.Hours) : 24 | "about an hour ago"; 25 | } 26 | else if (timeSpan <= TimeSpan.FromDays(30)) 27 | { 28 | result = timeSpan.Days > 1 ? 29 | String.Format("{0} days ago", timeSpan.Days) : 30 | "yesterday"; 31 | } 32 | else if (timeSpan <= TimeSpan.FromDays(365)) 33 | { 34 | result = timeSpan.Days > 30 ? 35 | String.Format("{0} months ago", timeSpan.Days / 30) : 36 | "about a month ago"; 37 | } 38 | else 39 | { 40 | result = timeSpan.Days > 365 ? 41 | String.Format("{0} years ago", timeSpan.Days / 365) : 42 | "a year ago"; 43 | } 44 | 45 | return result; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /server/src/WebApi/WebApi.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net7.0 5 | enable 6 | enable 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | all 16 | runtime; build; native; contentfiles; analyzers; buildtransitive 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/ProfileServiceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Tests; 2 | 3 | public class ProfileServiceTests 4 | { 5 | private readonly IUnitOfWork unitOfWork; 6 | private readonly IProfileRepository profileRepository; 7 | 8 | public ProfileServiceTests() 9 | { 10 | unitOfWork = A.Fake(); 11 | profileRepository = A.Fake(); 12 | A.CallTo(() => unitOfWork.Profile).Returns(profileRepository); 13 | } 14 | 15 | [Fact] 16 | public async Task Get_With_Data_Returns_ProfileDto() 17 | { 18 | // Arrange 19 | var existingProfile = A.Dummy(); 20 | A.CallTo(() => profileRepository.Get()).Returns(existingProfile); 21 | var profileService = new ProfileService(unitOfWork); 22 | 23 | // Act 24 | var result = await profileService.Get(); 25 | 26 | // Assert 27 | Assert.NotNull(result); 28 | Assert.IsType(result); 29 | } 30 | 31 | [Fact] 32 | public async Task Update_For_Successful_Update_Returns_True() 33 | { 34 | // Arrange 35 | var existingProfile = A.Dummy(); 36 | A.CallTo(() => profileRepository.Get()).Returns(existingProfile); 37 | var updatedProfileDto = A.Dummy(); 38 | updatedProfileDto.Id = existingProfile.Id; 39 | var profileService = new ProfileService(unitOfWork); 40 | 41 | // Act 42 | var result = await profileService.Update(updatedProfileDto); 43 | 44 | // Assert 45 | Assert.True(result); 46 | A.CallTo(() => unitOfWork.Profile.Update(A.That.Matches(p => p.Id == existingProfile.Id))).MustHaveHappenedOnceExactly(); 47 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /client/src/assets/admin/images/delete.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | 3 | # Vue Client Application 4 | portfolio_client: 5 | container_name: portfolio_client_container 6 | ports: 7 | - "8080:8080" 8 | build: 9 | context: ./client 10 | dockerfile: Dockerfile 11 | networks: 12 | - portfolio_network 13 | 14 | # ASP.NET Core Web API Application 15 | portfolio_api: 16 | container_name: portfolio_api_container 17 | ports: 18 | - "5000:5000" 19 | depends_on: 20 | - portfolio_db 21 | build: 22 | context: ./server 23 | dockerfile: Dockerfile 24 | environment: 25 | - ConnectionStrings__DefaultConnection=User ID=sara;Password=mysecretpassword;Server=portfolio_db_container;Port=5432;Database=PortfolioDB;IntegratedSecurity=true;Pooling=true; 26 | - ASPNETCORE_URLS=http://+:5000 27 | networks: 28 | - portfolio_network 29 | 30 | # PostgreSQL Database 31 | portfolio_db: 32 | container_name: portfolio_db_container 33 | image: postgres 34 | environment: 35 | POSTGRES_USER: sara 36 | POSTGRES_PASSWORD: mysecretpassword 37 | PGDATA: /data/postgres 38 | volumes: 39 | - postgres_data:/data/postgres 40 | ports: 41 | - "5433:5432" 42 | networks: 43 | - portfolio_network 44 | restart: unless-stopped 45 | 46 | # PGAdmin User Interface 47 | portfolio_pgadmin: 48 | container_name: portfolio_pgadmin_container 49 | image: dpage/pgadmin4 50 | environment: 51 | PGADMIN_DEFAULT_EMAIL: admin@gmail.com 52 | PGADMIN_DEFAULT_PASSWORD: mysecretpassword 53 | PGADMIN_CONFIG_SERVER_MODE: 'False' 54 | volumes: 55 | - pgadmin_data:/var/lib/pgadmin 56 | 57 | ports: 58 | - "8000:80" 59 | networks: 60 | - portfolio_network 61 | restart: unless-stopped 62 | 63 | networks: 64 | portfolio_network: 65 | driver: bridge 66 | 67 | volumes: 68 | postgres_data: 69 | pgadmin_data: -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/IntegrationTestWebApplicationFactory.cs: -------------------------------------------------------------------------------- 1 | using Infrastructure.DbContexts; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.AspNetCore.Mvc.Testing; 4 | using Microsoft.AspNetCore.TestHost; 5 | using Microsoft.EntityFrameworkCore; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Testcontainers.PostgreSql; 8 | 9 | namespace WebApi.Tests.Integration; 10 | 11 | public class IntegrationTestWebApplicationFactory : WebApplicationFactory, IAsyncLifetime 12 | { 13 | private readonly PostgreSqlContainer _dbContainer = new PostgreSqlBuilder() 14 | .WithImage("postgres:latest") 15 | .WithDatabase("PortfolioDb_Test") 16 | .WithUsername("postgres") 17 | .WithPassword("mysecretpassword") 18 | .Build(); 19 | 20 | public async Task InitializeAsync() 21 | { 22 | await _dbContainer.StartAsync(); 23 | 24 | using (var scope = Services.CreateScope()) 25 | { 26 | var scopedServices = scope.ServiceProvider; 27 | var cntx = scopedServices.GetRequiredService(); 28 | 29 | await cntx.Database.EnsureCreatedAsync(); 30 | } 31 | } 32 | 33 | public new async Task DisposeAsync() 34 | { 35 | await _dbContainer.StopAsync(); 36 | } 37 | 38 | protected override void ConfigureWebHost(IWebHostBuilder builder) 39 | { 40 | builder.ConfigureTestServices(services => 41 | { 42 | var descriptor = services.SingleOrDefault(s => s.ServiceType == typeof(DbContextOptions)); 43 | 44 | if (descriptor is not null) 45 | { 46 | services.Remove(descriptor); 47 | } 48 | 49 | services.AddDbContext(options => 50 | { 51 | options.UseNpgsql(_dbContainer.GetConnectionString()); 52 | }); 53 | }); 54 | } 55 | } 56 | 57 | -------------------------------------------------------------------------------- /client/src/components/home/Loading.vue: -------------------------------------------------------------------------------- 1 | 11 | 90 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/EducationsController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WebApi.Controllers; 7 | 8 | [Route("api/educations")] 9 | [ApiController] 10 | public class EducationsController : ControllerBase 11 | { 12 | private readonly IEducationService _educationService; 13 | public EducationsController(IEducationService educationService) 14 | { 15 | _educationService = educationService; 16 | } 17 | 18 | [HttpGet] 19 | public async Task Get() 20 | { 21 | var result = await _educationService.GetAll(); 22 | return Ok(result); 23 | } 24 | 25 | [HttpGet("{id:guid}")] 26 | [Authorize] 27 | public async Task Get([FromRoute] Guid id) 28 | { 29 | var result = await _educationService.GetById(id); 30 | if (result is null) return NoContent(); 31 | return Ok(result); 32 | } 33 | 34 | [HttpPost] 35 | [Authorize] 36 | public async Task Post([FromBody] EducationDto dto) 37 | { 38 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 39 | var result = await _educationService.Add(dto); 40 | return Ok(result); 41 | } 42 | 43 | [HttpPut("{id:guid}")] 44 | [Authorize] 45 | public async Task Put([FromRoute] Guid id, [FromBody] EducationDto dto) 46 | { 47 | if (!ModelState.IsValid) return BadRequest(ModelState); 48 | var result = await _educationService.Update(id, dto); 49 | if (!result) return BadRequest(); 50 | return Ok(); 51 | } 52 | 53 | [HttpDelete("{id:guid}")] 54 | [Authorize] 55 | public async Task Delete([FromRoute] Guid id) 56 | { 57 | var result = await _educationService.Delete(id); 58 | if (!result) return BadRequest(); 59 | return Ok(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/Application/Service/EducationService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Interfaces; 3 | using Application.Service.Interfaces; 4 | using Domain.Entities; 5 | using Mapster; 6 | 7 | namespace Application.Service; 8 | 9 | public class EducationService : IEducationService 10 | { 11 | private readonly IUnitOfWork _unitOfWork; 12 | public EducationService(IUnitOfWork unitOfWork) 13 | { 14 | _unitOfWork = unitOfWork; 15 | } 16 | 17 | public async Task> GetAll() 18 | { 19 | var result = await _unitOfWork.Education.GetAll(); 20 | return result.Adapt>(); 21 | } 22 | 23 | public async Task GetById(Guid id) 24 | { 25 | var result = await _unitOfWork.Education.GetById(id); 26 | return result?.Adapt(); 27 | } 28 | 29 | public async Task Add(EducationDto model) 30 | { 31 | Education toAdd = model.Adapt(); 32 | toAdd.Id = Guid.NewGuid(); 33 | var result = await _unitOfWork.Education.Add(toAdd); 34 | await _unitOfWork.SaveChangesAsync(); 35 | return result.Adapt(); 36 | } 37 | 38 | public async Task Update(Guid id, EducationDto model) 39 | { 40 | if (model is null || model.Id != id) return false; 41 | 42 | var toUpdate = await _unitOfWork.Education.GetById(id); 43 | if (toUpdate is null) return false; 44 | 45 | toUpdate = model.Adapt(); 46 | 47 | _unitOfWork.Education.Update(toUpdate); 48 | await _unitOfWork.SaveChangesAsync(); 49 | return true; 50 | } 51 | 52 | public async Task Delete(Guid id) 53 | { 54 | var toDelete = await _unitOfWork.Education.GetById(id); 55 | if (toDelete is null) return false; 56 | 57 | _unitOfWork.Education.Delete(toDelete); 58 | await _unitOfWork.SaveChangesAsync(); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/ExperiencesController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WebApi.Controllers; 7 | 8 | [Route("api/experiences")] 9 | [ApiController] 10 | public class ExperiencesController : ControllerBase 11 | { 12 | private readonly IExperienceService _experienceService; 13 | public ExperiencesController(IExperienceService experienceService) 14 | { 15 | _experienceService = experienceService; 16 | } 17 | 18 | [HttpGet] 19 | public async Task Get() 20 | { 21 | var result = await _experienceService.GetAll(); 22 | return Ok(result); 23 | } 24 | 25 | [HttpGet("{id:guid}")] 26 | [Authorize] 27 | public async Task Get([FromRoute] Guid id) 28 | { 29 | var result = await _experienceService.GetById(id); 30 | if (result is null) return NoContent(); 31 | return Ok(result); 32 | } 33 | 34 | [HttpPost] 35 | [Authorize] 36 | public async Task Post([FromBody] ExperienceDto dto) 37 | { 38 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 39 | var result = await _experienceService.Add(dto); 40 | return Ok(result); 41 | } 42 | 43 | [HttpPut("{id:guid}")] 44 | [Authorize] 45 | public async Task Put([FromRoute] Guid id, [FromBody] ExperienceDto dto) 46 | { 47 | if (!ModelState.IsValid) return BadRequest(ModelState); 48 | var result = await _experienceService.Update(id, dto); 49 | if (!result) return BadRequest(); 50 | return Ok(); 51 | } 52 | 53 | [HttpDelete("{id:guid}")] 54 | [Authorize] 55 | public async Task Delete([FromRoute] Guid id) 56 | { 57 | var result = await _experienceService.Delete(id); 58 | if (!result) return BadRequest(); 59 | return Ok(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/SocialLinksController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WebApi.Controllers; 7 | 8 | [Route("api/social-links")] 9 | [ApiController] 10 | public class SocialLinksController : ControllerBase 11 | { 12 | private readonly ISocialLinkService _socialLinkService; 13 | public SocialLinksController(ISocialLinkService socialLinkService) 14 | { 15 | _socialLinkService = socialLinkService; 16 | } 17 | 18 | [HttpGet] 19 | public async Task Get() 20 | { 21 | var result = await _socialLinkService.GetAll(); 22 | return Ok(result); 23 | } 24 | 25 | [HttpGet("{id:guid}")] 26 | [Authorize] 27 | public async Task Get([FromRoute] Guid id) 28 | { 29 | var result = await _socialLinkService.GetById(id); 30 | if (result is null) return NoContent(); 31 | return Ok(result); 32 | } 33 | 34 | [HttpPost] 35 | [Authorize] 36 | public async Task Post([FromBody] SocialLinkDto dto) 37 | { 38 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 39 | var result = await _socialLinkService.Add(dto); 40 | return Ok(result); 41 | } 42 | 43 | [HttpPut("{id:guid}")] 44 | [Authorize] 45 | public async Task Put([FromRoute] Guid id, [FromBody] SocialLinkDto dto) 46 | { 47 | if (!ModelState.IsValid) return BadRequest(ModelState); 48 | var result = await _socialLinkService.Update(id, dto); 49 | if (!result) return BadRequest(); 50 | return Ok(); 51 | } 52 | 53 | [HttpDelete("{id:guid}")] 54 | [Authorize] 55 | public async Task Delete([FromRoute] Guid id) 56 | { 57 | var result = await _socialLinkService.Delete(id); 58 | if (!result) return BadRequest(); 59 | return Ok(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/Application/Service/ExperienceService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Interfaces; 3 | using Application.Service.Interfaces; 4 | using Domain.Entities; 5 | using Mapster; 6 | 7 | namespace Application.Service; 8 | 9 | public class ExperienceService : IExperienceService 10 | { 11 | private readonly IUnitOfWork _unitOfWork; 12 | public ExperienceService(IUnitOfWork unitOfWork) 13 | { 14 | _unitOfWork = unitOfWork; 15 | } 16 | 17 | public async Task> GetAll() 18 | { 19 | var result = await _unitOfWork.Experience.GetAll(); 20 | return result.Adapt>(); 21 | } 22 | 23 | public async Task GetById(Guid id) 24 | { 25 | var result = await _unitOfWork.Experience.GetById(id); 26 | return result?.Adapt(); 27 | } 28 | 29 | public async Task Add(ExperienceDto model) 30 | { 31 | Experience toAdd = model.Adapt(); 32 | toAdd.Id = Guid.NewGuid(); 33 | var result = await _unitOfWork.Experience.Add(toAdd); 34 | await _unitOfWork.SaveChangesAsync(); 35 | return result.Adapt(); 36 | } 37 | 38 | public async Task Update(Guid id, ExperienceDto model) 39 | { 40 | if (model is null || model.Id != id) return false; 41 | 42 | var toUpdate = await _unitOfWork.Experience.GetById(id); 43 | if (toUpdate is null) return false; 44 | 45 | toUpdate = model.Adapt(); 46 | 47 | _unitOfWork.Experience.Update(toUpdate); 48 | await _unitOfWork.SaveChangesAsync(); 49 | return true; 50 | } 51 | 52 | public async Task Delete(Guid id) 53 | { 54 | var toDelete = await _unitOfWork.Experience.GetById(id); 55 | if (toDelete is null) return false; 56 | 57 | _unitOfWork.Experience.Delete(toDelete); 58 | await _unitOfWork.SaveChangesAsync(); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/Application/Service/SocialLinkService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Interfaces; 3 | using Application.Service.Interfaces; 4 | using Domain.Entities; 5 | using Mapster; 6 | 7 | namespace Application.Service; 8 | 9 | public class SocialLinkService : ISocialLinkService 10 | { 11 | private readonly IUnitOfWork _unitOfWork; 12 | public SocialLinkService(IUnitOfWork unitOfWork) 13 | { 14 | _unitOfWork = unitOfWork; 15 | } 16 | 17 | public async Task> GetAll() 18 | { 19 | var result = await _unitOfWork.SocialLink.GetAll(); 20 | return result.Adapt>(); 21 | } 22 | 23 | public async Task GetById(Guid id) 24 | { 25 | var result = await _unitOfWork.SocialLink.GetById(id); 26 | return result?.Adapt(); 27 | } 28 | 29 | public async Task Add(SocialLinkDto model) 30 | { 31 | SocialLink toAdd = model.Adapt(); 32 | toAdd.Id = Guid.NewGuid(); 33 | var result = await _unitOfWork.SocialLink.Add(toAdd); 34 | await _unitOfWork.SaveChangesAsync(); 35 | return result.Adapt(); 36 | } 37 | 38 | public async Task Update(Guid id, SocialLinkDto model) 39 | { 40 | if (model is null || model.Id != id) return false; 41 | 42 | var toUpdate = await _unitOfWork.SocialLink.GetById(id); 43 | if (toUpdate is null) return false; 44 | 45 | toUpdate = model.Adapt(); 46 | 47 | _unitOfWork.SocialLink.Update(toUpdate); 48 | await _unitOfWork.SaveChangesAsync(); 49 | return true; 50 | } 51 | 52 | public async Task Delete(Guid id) 53 | { 54 | var toDelete = await _unitOfWork.SocialLink.GetById(id); 55 | if (toDelete is null) return false; 56 | 57 | _unitOfWork.SocialLink.Delete(toDelete); 58 | await _unitOfWork.SaveChangesAsync(); 59 | return true; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/WebApi/Controllers/MessagesController.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Service.Interfaces; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace WebApi.Controllers; 7 | 8 | [Route("api/messages")] 9 | [ApiController] 10 | public class MessagesController : ControllerBase 11 | { 12 | private readonly IMessageService _messageService; 13 | public MessagesController(IMessageService messageService) 14 | { 15 | _messageService = messageService; 16 | } 17 | 18 | [HttpGet] 19 | [Authorize] 20 | public async Task Get() 21 | { 22 | var result = await _messageService.GetAll(); 23 | return Ok(result); 24 | } 25 | 26 | [HttpGet("unread")] 27 | [Authorize] 28 | public async Task GetNumberOfUnread() 29 | { 30 | var result = await _messageService.GetNumberOfUnread(); 31 | return Ok(result); 32 | } 33 | 34 | [HttpGet("{id:guid}")] 35 | [Authorize] 36 | public async Task Get([FromRoute] Guid id) 37 | { 38 | var result = await _messageService.GetById(id); 39 | if (result is null) return NoContent(); 40 | return Ok(result); 41 | } 42 | 43 | [HttpPost] 44 | public async Task Post([FromBody] MessageDto dto) 45 | { 46 | if (!ModelState.IsValid || dto is null) return BadRequest(ModelState); 47 | var result = await _messageService.Add(dto); 48 | return Ok(result); 49 | } 50 | 51 | [HttpDelete("{id:guid}")] 52 | [Authorize] 53 | public async Task Delete([FromRoute] Guid id) 54 | { 55 | var result = await _messageService.Delete(id); 56 | if (!result) return BadRequest(); 57 | return Ok(); 58 | } 59 | 60 | [HttpPut("mark-as-read/{id:guid}")] 61 | [Authorize] 62 | public async Task MarkAsRead([FromRoute] Guid id) 63 | { 64 | var result = await _messageService.MarkAsRead(id); 65 | if (!result) return BadRequest(); 66 | return Ok(); 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /server/src/WebApi/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.JwtBearer; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.IdentityModel.Tokens; 4 | using System.Text; 5 | 6 | var builder = WebApplication.CreateBuilder(args); 7 | 8 | builder.Services.AddCors(); 9 | builder.Services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 10 | 11 | // Add services to the container. 12 | 13 | builder.Services.AddAuthentication(options => 14 | { 15 | options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; 16 | options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; 17 | options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme; 18 | }).AddJwtBearer(o => 19 | { 20 | o.TokenValidationParameters = new TokenValidationParameters 21 | { 22 | ValidIssuer = builder.Configuration["JwtSettings:Issuer"], 23 | ValidAudience = builder.Configuration["JwtSettings:Audience"], 24 | IssuerSigningKey = new SymmetricSecurityKey 25 | (Encoding.UTF8.GetBytes(builder.Configuration["JwtSettings:Key"])), 26 | ValidateIssuer = true, 27 | ValidateAudience = true, 28 | ValidateLifetime = true, 29 | ClockSkew = TimeSpan.FromMinutes(5), 30 | ValidateIssuerSigningKey = true 31 | }; 32 | }); 33 | 34 | builder.Services.AddAuthorization(); 35 | 36 | builder.Services.AddControllers(); 37 | 38 | builder.Services.AddInfrastructureServices(builder.Configuration); 39 | builder.Services.AddApplicationServices(); 40 | 41 | builder.Services.AddEndpointsApiExplorer(); 42 | builder.Services.AddSwaggerGen(); 43 | 44 | var app = builder.Build(); 45 | 46 | // Enable CORS for the client app running inside a Docker container 47 | app.UseCors(options => options.WithOrigins("http://localhost:8080").AllowAnyHeader().AllowAnyMethod()); 48 | 49 | // Configure the HTTP request pipeline. 50 | if (app.Environment.IsDevelopment()) 51 | { 52 | app.UseSwagger(); 53 | app.UseSwaggerUI(); 54 | } 55 | 56 | app.UseHttpsRedirection(); 57 | 58 | app.UseAuthentication(); 59 | app.UseAuthorization(); 60 | 61 | app.MapControllers(); 62 | 63 | app.Run(); 64 | 65 | public partial class Program { } -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/TimeExtensionsTests.cs: -------------------------------------------------------------------------------- 1 | using Application.Extentions; 2 | 3 | namespace Application.Tests 4 | { 5 | public class TimeExtensionsTests 6 | { 7 | [Fact] 8 | public void TimeAgo_Returns_SecondsAgo_When_Time_Span_Is_Within_One_Minute() 9 | { 10 | //Arrange 11 | var now = DateTime.UtcNow; 12 | var dateTime = now.AddSeconds(-30); 13 | 14 | //Act 15 | var result = dateTime.TimeAgo(); 16 | 17 | //Assert 18 | Assert.Equal("30 seconds ago", result); 19 | } 20 | 21 | [Fact] 22 | public void TimeAgo_Returns_About_A_Minute_Ago_When_Time_Span_Is_One_Minute() 23 | { 24 | //Arrange 25 | var now = DateTime.UtcNow; 26 | var dateTime = now.AddMinutes(-1); 27 | 28 | //Act 29 | var result = dateTime.TimeAgo(); 30 | 31 | //Assert 32 | Assert.Equal("about a minute ago", result); 33 | } 34 | 35 | [Fact] 36 | public void TimeAgo_Returns_HoursAgo_When_Time_Span_Is_Within_One_Day() 37 | { 38 | //Arrange 39 | var now = DateTime.UtcNow; 40 | var dateTime = now.AddHours(-6); 41 | 42 | //Act 43 | var result = dateTime.TimeAgo(); 44 | 45 | //Assert 46 | Assert.Equal("6 hours ago", result); 47 | } 48 | 49 | [Fact] 50 | public void TimeAgo_Returns_DaysAgo_When_Time_Span_Is_Within_One_Month() 51 | { 52 | //Arrange 53 | var now = DateTime.UtcNow; 54 | var dateTime = now.AddDays(-12); 55 | 56 | //Act 57 | var result = dateTime.TimeAgo(); 58 | 59 | //Assert 60 | Assert.Equal("12 days ago", result); 61 | } 62 | 63 | [Fact] 64 | public void TimeAgo_Returns_YearsAgo_When_Time_Span_Is_Years_Ago() 65 | { 66 | //Arrange 67 | var now = DateTime.UtcNow; 68 | var dateTime = now.AddYears(-2); 69 | 70 | //Act 71 | var result = dateTime.TimeAgo(); 72 | 73 | //Assert 74 | Assert.Equal("2 years ago", result); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /server/src/Application/Service/MessageService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Extentions; 3 | using Application.Interfaces; 4 | using Application.Service.Interfaces; 5 | using Domain.Entities; 6 | using Mapster; 7 | 8 | namespace Application.Service; 9 | 10 | public class MessageService : IMessageService 11 | { 12 | private readonly IUnitOfWork _unitOfWork; 13 | public MessageService(IUnitOfWork unitOfWork) 14 | { 15 | _unitOfWork = unitOfWork; 16 | } 17 | 18 | public async Task> GetAll() 19 | { 20 | var result = await _unitOfWork.Message.GetAll(); 21 | return result.Adapt>(); 22 | } 23 | 24 | public async Task GetNumberOfUnread() 25 | { 26 | return await _unitOfWork.Message.GetNumberOfUnread(); 27 | } 28 | 29 | public async Task GetById(Guid id) 30 | { 31 | var result = await _unitOfWork.Message.GetById(id); 32 | if (result is null) return null; 33 | 34 | MessageDto dto = result.Adapt(); 35 | 36 | dto.SentAt = result.SentAt.ToString("dd MMMM yyyy"); 37 | dto.TimeAgo = result.SentAt.TimeAgo(); 38 | return dto; 39 | } 40 | 41 | public async Task Add(MessageDto model) 42 | { 43 | Message toAdd = model.Adapt(); 44 | toAdd.Id = Guid.NewGuid(); 45 | toAdd.SentAt = DateTime.UtcNow; 46 | var result = await _unitOfWork.Message.Add(toAdd); 47 | await _unitOfWork.SaveChangesAsync(); 48 | return result.Adapt(); 49 | } 50 | 51 | public async Task Delete(Guid id) 52 | { 53 | var toDelete = await _unitOfWork.Message.GetById(id); 54 | if (toDelete is null) return false; 55 | 56 | _unitOfWork.Message.Delete(toDelete); 57 | await _unitOfWork.SaveChangesAsync(); 58 | return true; 59 | } 60 | 61 | public async Task MarkAsRead(Guid id) 62 | { 63 | var toUpdate = await _unitOfWork.Message.GetById(id); 64 | if (toUpdate is null) return false; 65 | 66 | toUpdate.IsRead = true; 67 | 68 | _unitOfWork.Message.Update(toUpdate); 69 | await _unitOfWork.SaveChangesAsync(); 70 | return true; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/IdentityControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class IdentityControllerTests : BaseControllerTest 4 | { 5 | public IdentityControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task Login_Returns_OK_With_Token() 10 | { 11 | // Arrange 12 | // Using the one user created by database seed 13 | UserLoginDto userLoginDto = new UserLoginDto 14 | { 15 | UserName = "admin", 16 | Password = "123456" 17 | }; 18 | 19 | // Act 20 | var loginResponse = await _httpClient.PostAsJsonAsync("api/identity/login", userLoginDto); 21 | 22 | // Assert 23 | loginResponse.EnsureSuccessStatusCode(); 24 | 25 | string token = await loginResponse.Content.ReadAsStringAsync(); 26 | token.Should().NotBeEmpty(); 27 | token.Should().NotBeNull(); 28 | } 29 | 30 | [Fact] 31 | public async Task Login_With_Wrong_Password_Returns_NoContent() 32 | { 33 | // Arrange 34 | UserLoginDto userLoginDto = new UserLoginDto 35 | { 36 | UserName = "admin", 37 | Password = "aaa" 38 | }; 39 | 40 | // Act 41 | var loginResponse = await _httpClient.PostAsJsonAsync("api/identity/login", userLoginDto); 42 | 43 | // Assert 44 | loginResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); 45 | 46 | string token = await loginResponse.Content.ReadAsStringAsync(); 47 | token.Should().BeEmpty(); 48 | } 49 | 50 | [Fact] 51 | public async Task ChangePassword_Returns_OK() 52 | { 53 | // Arrange 54 | await AuthenticateAsync(); 55 | 56 | // Using the one user created by database seed 57 | PasswordDto passwordDto = new PasswordDto 58 | { 59 | currentPassword = "123456", 60 | newPassword = "admin123456", 61 | confirmNewPassword = "admin123456", 62 | }; 63 | 64 | // Act 65 | var response = await _httpClient.PutAsJsonAsync("api/identity/change-password", passwordDto); 66 | 67 | // Assert 68 | response.EnsureSuccessStatusCode(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /server/src/Infrastructure/DbContexts/PortfolioDbContext.cs: -------------------------------------------------------------------------------- 1 | using Domain.Entities; 2 | using Microsoft.EntityFrameworkCore; 3 | using System.Reflection; 4 | 5 | namespace Infrastructure.DbContexts; 6 | 7 | public class PortfolioDbContext : DbContext 8 | { 9 | public PortfolioDbContext(DbContextOptions options) : base(options) { } 10 | 11 | public DbSet Users { get; set; } 12 | public DbSet Profiles { get; set; } 13 | public DbSet Experiences { get; set; } 14 | public DbSet Educations { get; set; } 15 | public DbSet SocialLinks { get; set; } 16 | public DbSet Messages { get; set; } 17 | 18 | protected override void OnModelCreating(ModelBuilder modelBuilder) 19 | { 20 | modelBuilder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); 21 | 22 | // Seed 23 | modelBuilder.Entity().HasData( 24 | new Profile 25 | { 26 | Id = Guid.NewGuid(), 27 | FirstName = "Sara", 28 | LastName = "Rasoulian", 29 | Email = "example@gmail.com", 30 | Headline = "Lorem ipsum", 31 | About = "Lorem ipsum is a placeholder text.", 32 | } 33 | ); 34 | 35 | modelBuilder.Entity().HasData( 36 | new User 37 | { 38 | Id = Guid.NewGuid(), 39 | UserName = "admin", 40 | Password = "123456", 41 | Email = "example@gmail.com" 42 | } 43 | ); 44 | 45 | modelBuilder.Entity().HasData( 46 | new Experience 47 | { 48 | Id = Guid.NewGuid(), 49 | CompanyName = "Test", 50 | StartYear = "2020", 51 | EndYear = "2022", 52 | Description = "Lorem ipsum is a placeholder text." 53 | } 54 | ); 55 | 56 | modelBuilder.Entity().HasData( 57 | new Education 58 | { 59 | Id = Guid.NewGuid(), 60 | Degree = "Bachelor's degree", 61 | FieldOfStudy = "Software Engineering", 62 | School = "Test University", 63 | StartYear = "2016", 64 | EndYear = "2020", 65 | Description = "Lorem ipsum is a placeholder text." 66 | } 67 | ); 68 | 69 | modelBuilder.Entity().HasData( 70 | new SocialLink 71 | { 72 | Id = Guid.NewGuid(), 73 | Name = "Github", 74 | URL = "https://github.com/SaraRasoulian", 75 | } 76 | ); 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /client/src/components/home/SendMessage.vue: -------------------------------------------------------------------------------- 1 | 40 | 41 | -------------------------------------------------------------------------------- /server/src/Application/Service/IdentityService.cs: -------------------------------------------------------------------------------- 1 | using Application.DTOs; 2 | using Application.Interfaces; 3 | using Application.Service.Interfaces; 4 | using Domain.Entities; 5 | using Microsoft.Extensions.Configuration; 6 | using Microsoft.IdentityModel.Tokens; 7 | using System.IdentityModel.Tokens.Jwt; 8 | using System.Security.Claims; 9 | using System.Text; 10 | 11 | namespace Application.Service; 12 | 13 | public class IdentityService : IIdentityService 14 | { 15 | private readonly IUnitOfWork _unitOfWork; 16 | private readonly IConfiguration _config; 17 | public IdentityService(IUnitOfWork unitOfWork, IConfiguration config) 18 | { 19 | _unitOfWork = unitOfWork; 20 | _config = config; 21 | } 22 | 23 | public async Task Login(UserLoginDto userLogin) 24 | { 25 | User? user = await _unitOfWork.User.Get(userLogin.UserName, userLogin.Password); 26 | 27 | if (user == null) return null; 28 | 29 | return GenerateToken(user); 30 | } 31 | 32 | public async Task ChangePassword(PasswordDto dto, string userName) 33 | { 34 | if (dto.newPassword != dto.confirmNewPassword) return false; 35 | 36 | User? user = await _unitOfWork.User.GetByUserName(userName); 37 | 38 | if (user == null || user.Password != dto.currentPassword) return false; 39 | 40 | user.Password = dto.newPassword; 41 | 42 | _unitOfWork.User.Update(user); 43 | await _unitOfWork.SaveChangesAsync(); 44 | return true; 45 | } 46 | 47 | public string GenerateToken(User user) 48 | { 49 | var issuer = _config["JwtSettings:Issuer"]; 50 | var audience = _config["JwtSettings:Audience"]; 51 | var key = Encoding.ASCII.GetBytes(_config["JwtSettings:Key"]); 52 | 53 | var tokenDescriptor = new SecurityTokenDescriptor 54 | { 55 | Subject = new ClaimsIdentity(new[] 56 | { 57 | new Claim("Id", Guid.NewGuid().ToString()), 58 | new Claim(JwtRegisteredClaimNames.Sub, user.UserName), 59 | new Claim(JwtRegisteredClaimNames.Email, user.Email), 60 | new Claim(JwtRegisteredClaimNames.Jti, 61 | Guid.NewGuid().ToString()) 62 | }), 63 | Expires = DateTime.UtcNow.AddMinutes(10), 64 | Issuer = issuer, 65 | Audience = audience, 66 | SigningCredentials = new SigningCredentials 67 | (new SymmetricSecurityKey(key), 68 | SecurityAlgorithms.HmacSha512Signature) 69 | }; 70 | var tokenHandler = new JwtSecurityTokenHandler(); 71 | var token = tokenHandler.CreateToken(tokenDescriptor); 72 | var stringToken = tokenHandler.WriteToken(token); 73 | return stringToken; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /client/src/views/home/Home.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 33 | 34 | 76 | -------------------------------------------------------------------------------- /client/src/views/admin/messages/ViewMessage.vue: -------------------------------------------------------------------------------- 1 | 46 | 47 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/MessageServiceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Tests; 2 | 3 | public class MessageServiceTests 4 | { 5 | private readonly IUnitOfWork unitOfWork; 6 | private readonly IMessageRepository messageRepository; 7 | 8 | public MessageServiceTests() 9 | { 10 | unitOfWork = A.Fake(); 11 | messageRepository = A.Fake(); 12 | A.CallTo(() => unitOfWork.Message).Returns(messageRepository); 13 | } 14 | 15 | [Fact] 16 | public async Task GetAll_With_Data_Returns_List_Of_MessageDto() 17 | { 18 | // Arrange 19 | var messageData = new List(); 20 | A.CallTo(() => messageRepository.GetAll()).Returns(messageData); 21 | var messageService = new MessageService(unitOfWork); 22 | 23 | // Act 24 | var result = await messageService.GetAll(); 25 | 26 | // Assert 27 | Assert.NotNull(result); 28 | Assert.IsAssignableFrom>(result); 29 | Assert.Equal(messageData.Count, result.Count()); 30 | } 31 | 32 | [Fact] 33 | public async Task GetById_With_Data_Returns_MessageDto() 34 | { 35 | // Arrange 36 | var messageId = Guid.NewGuid(); 37 | var messageData = A.Dummy(); 38 | A.CallTo(() => messageRepository.GetById(messageId)).Returns(messageData); 39 | var messageService = new MessageService(unitOfWork); 40 | 41 | // Act 42 | var result = await messageService.GetById(messageId); 43 | 44 | // Assert 45 | Assert.NotNull(result); 46 | Assert.IsType(result); 47 | } 48 | 49 | [Fact] 50 | public async Task Add_Returns_Added_MessageDto() 51 | { 52 | // Arrange 53 | var messageDto = A.Dummy(); 54 | var addedMessage = messageDto.Adapt(); 55 | addedMessage.Id = Guid.NewGuid(); 56 | A.CallTo(() => messageRepository.Add(A._)).Returns(addedMessage); 57 | 58 | var messageService = new MessageService(unitOfWork); 59 | 60 | // Act 61 | var result = await messageService.Add(messageDto); 62 | 63 | // Assert 64 | Assert.NotNull(result); 65 | Assert.IsType(result); 66 | Assert.Equal(addedMessage.Id, result.Id); 67 | } 68 | 69 | [Fact] 70 | public async Task Delete_With_Existing_Id_Returns_True() 71 | { 72 | // Arrange 73 | var existingId = Guid.NewGuid(); 74 | var existingMessage = A.Dummy(); 75 | A.CallTo(() => messageRepository.GetById(existingId)).Returns(existingMessage); 76 | 77 | var messageService = new MessageService(unitOfWork); 78 | 79 | // Act 80 | var result = await messageService.Delete(existingId); 81 | 82 | // Assert 83 | Assert.True(result); 84 | A.CallTo(() => unitOfWork.Message.Delete(existingMessage)).MustHaveHappenedOnceExactly(); 85 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Personal Portfolio Website ⚡

2 | 3 | A portfolio website with admin panel. The backend is built with **ASP.NET Core Web API**, the frontend with **Vue JS**, and the database with **PostgreSQL**. 4 | 5 | This is a __learning project__ and it showcases the implementation of several software development practices such as clean architecture, design patterns and test-driven development. 6 | 7 | In a real-world scenario, these practices should be chosen based on the specific requirements of each project. 8 | 9 | 10 | 11 |

Demo 🚀

12 | 13 |
Home page
14 | 15 | ![Home -](https://github.com/SaraRasoulian/DotNet-Vue-Portfolio-Website/assets/51083712/b2872aeb-51c0-4452-8e6d-f7df9892b33c) 16 | 17 |
Admin panel
18 | 19 | ![Admin new](https://github.com/SaraRasoulian/DotNet-Vue-Portfolio-Website/assets/51083712/dbd59886-8985-4481-8e93-1dedbf5b2219) 20 | 21 | 22 |

Admin Panel Sections

23 | 24 | * Profile 25 | * Experiences 26 | * Educations 27 | * Social links 28 | * Messages 29 | * Login 30 | * Change password 31 | 32 |
33 | 34 | Screenshots of Admin Panel 📸 35 | 36 | 37 | ![Login page](https://github.com/SaraRasoulian/.Net-Vue-Portfolio/assets/51083712/a323b561-3f06-4b10-9335-6c9579960bec) 38 | 39 | ![Social links page](https://github.com/SaraRasoulian/.Net-Vue-Portfolio/assets/51083712/8e4fbea7-fe34-4f85-b73f-3f0145085dfb) 40 | 41 | ![Messages page](https://github.com/SaraRasoulian/.Net-Vue-Portfolio/assets/51083712/e92fd6a6-6ea2-422b-813c-0f8bad2c119d) 42 | 43 | 44 | ![Admin panel - Mobile](https://github.com/SaraRasoulian/.Net-Vue-Portfolio/assets/51083712/81ad5b85-9777-4108-b9e9-8a2b6abb9845) 45 | 46 | 47 |
48 | 49 | 50 |

Tech Stack 🛠️

51 | 52 | - ### Back End 53 | - ASP.NET Core Web API 54 | - .NET -v7 55 | - Entity Framework Core -v7 56 | - Mapster -v7 for object mapping 57 | - JWT (JSON Web Token) 58 | - Clean Architecture 59 | - Repository Service Pattern 60 | - Unit of Work Pattern 61 | - TDD (Test-Driven Development) 62 | - FakeItEasy for mocking & fake data generating 63 | - xUnit for unit and integration testing 64 | - Testcontainers for integration testing 65 | - Fluent Assertions 66 | 67 | - ### Front End 68 | - Vue JS -v3 (Vite-based) 69 | - Bootstrap -v5 for admin panel UI 70 | - Axios for API calls 71 | - Vuelidate for client-side validation 72 | - Vue-toastification for notifications 73 | - Skeleton for home page UI 74 | - HTML -v5 75 | - CSS -v3 76 | 77 | - ### Database 78 | - PostgreSQL -v15 79 | - Database built via Entity framework migrations (code-first approach) 80 | 81 | - ### IDE 82 | - Visual Studio 2022 -v17 for back end development 83 | - Visual Studio Code for front end development 84 | 85 | 86 |

Assets 🗃️

87 | 88 | To download the project's UI/UX design file [click here](https://github.com/SaraRasoulian/DotNet-Vue-Portfolio-Website/files/14462479/Home.Page.Admin.Panel.zip). 89 | Adobe XD software is required for viewing. 90 | 91 | To download the project's database design file [click here](https://github.com/SaraRasoulian/.Net-Vue-Portfolio/files/14537053/Portfolio-Database-Design.pdf). 92 | 93 |

Contributions 🤝

94 |

Contributions are appreciated. If you identify areas for improvement, please feel free to create an issue or submit a pull request. For any questions or suggestions, please create an issue.

95 | 96 | -------------------------------------------------------------------------------- /client/src/views/admin/messages/DeleteMessage.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | -------------------------------------------------------------------------------- /client/src/views/admin/educations/EducationList.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /client/src/views/admin/socialLinks/SocialList.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | -------------------------------------------------------------------------------- /client/src/views/admin/experiences/ExperienceList.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/MessagesControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class MessagesControllerTests : BaseControllerTest 4 | { 5 | public MessagesControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task GetAll_Returns_SuccessStatusCode() 10 | { 11 | // Arrange 12 | await AuthenticateAsync(); 13 | 14 | // Act 15 | var response = await _httpClient.GetAsync("api/messages"); 16 | 17 | // Assert 18 | response.EnsureSuccessStatusCode(); 19 | } 20 | 21 | [Fact] 22 | public async Task GetById_Returns_SuccessStatusCode() 23 | { 24 | // Arrange 25 | await AuthenticateAsync(); 26 | 27 | // Get a random id 28 | Guid id = Guid.NewGuid(); 29 | 30 | // Act 31 | var response = await _httpClient.GetAsync("api/messages/" + id); 32 | 33 | // Assert 34 | response.EnsureSuccessStatusCode(); 35 | response.Should().HaveStatusCode(HttpStatusCode.NoContent); 36 | } 37 | 38 | [Fact] 39 | public async Task GetNumberOfUnread_Returns_SuccessStatusCode() 40 | { 41 | // Arrange 42 | await AuthenticateAsync(); 43 | 44 | // Act 45 | var response = await _httpClient.GetAsync("api/messages/unread"); 46 | 47 | // Assert 48 | response.EnsureSuccessStatusCode(); 49 | } 50 | 51 | [Fact] 52 | public async Task Post_Returns_OK_With_MessageDto() 53 | { 54 | // Arrange 55 | await AuthenticateAsync(); 56 | MessageDto dto = new MessageDto 57 | { 58 | Name = "Sahra Farahani", 59 | Email = "farahani@gmail.com", 60 | Content = "Lorem ipsum is a placeholder text." 61 | }; 62 | 63 | // Act 64 | var response = await _httpClient.PostAsJsonAsync("/api/messages", dto); 65 | 66 | // Assert 67 | response.EnsureSuccessStatusCode(); 68 | 69 | var responseDto = await response.Content.ReadFromJsonAsync(); 70 | responseDto.Name.Should().Be(dto.Name); 71 | responseDto.Email.Should().Be(dto.Email); 72 | responseDto.Content.Should().Be(dto.Content); 73 | responseDto.IsRead.Should().BeFalse(); 74 | 75 | // Should return 1 unread message 76 | var unReadResponse = await _httpClient.GetAsync("api/messages/unread"); 77 | var numberOfUnread = await unReadResponse.Content.ReadAsStringAsync(); 78 | numberOfUnread.Should().Be("1"); 79 | } 80 | 81 | [Fact] 82 | public async Task Delete_With_Valid_Id_Returns_SuccessStatusCode() 83 | { 84 | // Arrange 85 | await AuthenticateAsync(); 86 | 87 | // Create a new message 88 | MessageDto dto = new MessageDto 89 | { 90 | Name = "Sahra Farahani", 91 | Email = "farahani@gmail.com", 92 | Content = "Lorem ipsum is a placeholder text." 93 | }; 94 | 95 | var postResponse = await _httpClient.PostAsJsonAsync("/api/messages", dto); 96 | var addedMessage = await postResponse.Content.ReadFromJsonAsync(); 97 | 98 | // Act 99 | var deleteResponse = await _httpClient.DeleteAsync("/api/messages/" + addedMessage.Id); 100 | 101 | // Assert 102 | postResponse.EnsureSuccessStatusCode(); 103 | deleteResponse.EnsureSuccessStatusCode(); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /client/src/views/admin/Login.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 58 | 59 | 117 | -------------------------------------------------------------------------------- /client/src/views/admin/messages/MessageList.vue: -------------------------------------------------------------------------------- 1 | 61 | 62 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/EducationsControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class EducationsControllerTests : BaseControllerTest 4 | { 5 | public EducationsControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task GetAll_Returns_SuccessStatusCode() 10 | { 11 | // Arrange 12 | // Act 13 | var response = await _httpClient.GetAsync("api/educations"); 14 | 15 | // Assert 16 | response.EnsureSuccessStatusCode(); 17 | } 18 | 19 | [Fact] 20 | public async Task GetById_Returns_SuccessStatusCode() 21 | { 22 | // Arrange 23 | await AuthenticateAsync(); 24 | 25 | // Get a random id 26 | Guid id = Guid.NewGuid(); 27 | 28 | // Act 29 | var response = await _httpClient.GetAsync("api/educations/" + id); 30 | 31 | // Assert 32 | response.EnsureSuccessStatusCode(); 33 | response.Should().HaveStatusCode(HttpStatusCode.NoContent); 34 | } 35 | 36 | [Fact] 37 | public async Task Post_Returns_OK_With_EducationDto() 38 | { 39 | // Arrange 40 | await AuthenticateAsync(); 41 | EducationDto dto = new EducationDto 42 | { 43 | Degree = "Master's degree", 44 | FieldOfStudy = "Artificial Intelligence", 45 | StartYear = "2020", 46 | EndYear = "2022", 47 | School = "Solstice College" 48 | }; 49 | 50 | // Act 51 | var response = await _httpClient.PostAsJsonAsync("/api/educations", dto); 52 | 53 | // Assert 54 | response.EnsureSuccessStatusCode(); 55 | 56 | var responseDto = await response.Content.ReadFromJsonAsync(); 57 | dto.Should().BeEquivalentTo(responseDto, options => options.Excluding(x => x.Id)); 58 | } 59 | 60 | [Fact] 61 | public async void Put_Returns_SuccessStatusCode() 62 | { 63 | // Arrange 64 | // Create a new education 65 | await AuthenticateAsync(); 66 | EducationDto dto = new EducationDto 67 | { 68 | Degree = "Master's degree", 69 | FieldOfStudy = "Artificial Intelligence", 70 | StartYear = "2020", 71 | EndYear = "2022", 72 | School = "Solstice College" 73 | }; 74 | 75 | var postResponse = await _httpClient.PostAsJsonAsync("/api/educations", dto); 76 | var addedEducation = await postResponse.Content.ReadFromJsonAsync(); 77 | 78 | // Update the added education 79 | addedEducation.Degree = "Bachelor's degree"; 80 | 81 | // Act 82 | var updateResponse = await _httpClient.PutAsJsonAsync("/api/educations/" + addedEducation.Id, addedEducation); 83 | 84 | // Assert 85 | updateResponse.EnsureSuccessStatusCode(); 86 | postResponse.EnsureSuccessStatusCode(); 87 | } 88 | 89 | [Fact] 90 | public async Task Delete_With_Valid_Id_Returns_SuccessStatusCode() 91 | { 92 | // Arrange 93 | // Create a new education 94 | await AuthenticateAsync(); 95 | EducationDto dto = new EducationDto 96 | { 97 | Degree = "Master's degree", 98 | FieldOfStudy = "Artificial Intelligence", 99 | StartYear = "2020", 100 | EndYear = "2022", 101 | School = "Solstice College" 102 | }; 103 | 104 | var postResponse = await _httpClient.PostAsJsonAsync("/api/educations", dto); 105 | var addedEducation = await postResponse.Content.ReadFromJsonAsync(); 106 | 107 | // Act 108 | var deleteResponse = await _httpClient.DeleteAsync("/api/educations/" + addedEducation.Id); 109 | 110 | // Assert 111 | postResponse.EnsureSuccessStatusCode(); 112 | deleteResponse.EnsureSuccessStatusCode(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /client/src/views/admin/socialLinks/ViewSocial.vue: -------------------------------------------------------------------------------- 1 | 69 | 70 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/SocialLinksControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class SocialLinksControllerTests : BaseControllerTest 4 | { 5 | public SocialLinksControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task GetAll_Returns_SuccessStatusCode() 10 | { 11 | // Act 12 | var response = await _httpClient.GetAsync("api/social-links"); 13 | 14 | // Assert 15 | response.EnsureSuccessStatusCode(); 16 | } 17 | 18 | [Fact] 19 | public async Task GetById_Returns_SuccessStatusCode() 20 | { 21 | // Arrange 22 | await AuthenticateAsync(); 23 | 24 | // Get a random id 25 | Guid id = Guid.NewGuid(); 26 | 27 | // Act 28 | var response = await _httpClient.GetAsync("api/social-links/" + id); 29 | 30 | // Assert 31 | response.EnsureSuccessStatusCode(); 32 | response.Should().HaveStatusCode(HttpStatusCode.NoContent); 33 | } 34 | 35 | [Fact] 36 | public async Task Unauthorized_GetById_Returns_UnauthorizedStatusCode() 37 | { 38 | // Arrange 39 | Guid id = Guid.NewGuid(); 40 | 41 | // Act 42 | var response = await _httpClient.GetAsync("api/social-links/" + id); 43 | 44 | // Assert 45 | response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); 46 | } 47 | 48 | [Fact] 49 | public async Task Post_Returns_OK_With_SocialLinkDto() 50 | { 51 | // Arrange 52 | await AuthenticateAsync(); 53 | SocialLinkDto dto = new SocialLinkDto 54 | { 55 | Name = "GitHub", 56 | URL = "https://github.com/SaraRasoulian" 57 | }; 58 | 59 | // Act 60 | var response = await _httpClient.PostAsJsonAsync("/api/social-links", dto); 61 | 62 | // Assert 63 | response.EnsureSuccessStatusCode(); 64 | 65 | var responseDto = await response.Content.ReadFromJsonAsync(); 66 | dto.Should().BeEquivalentTo(responseDto, options => options.Excluding(x => x.Id)); 67 | } 68 | 69 | [Fact] 70 | public async void Put_Returns_SuccessStatusCode() 71 | { 72 | // Arrange 73 | // Create a new social link 74 | await AuthenticateAsync(); 75 | SocialLinkDto dto = new SocialLinkDto 76 | { 77 | Name = "GitHub", 78 | URL = "https://github.com/SaraRasoulian" 79 | }; 80 | 81 | var postResponse = await _httpClient.PostAsJsonAsync("/api/social-links", dto); 82 | var addedSocialLink = await postResponse.Content.ReadFromJsonAsync(); 83 | 84 | // Update the added social link 85 | addedSocialLink.Name = "GitLab"; 86 | 87 | // Act 88 | var updateResponse = await _httpClient.PutAsJsonAsync("/api/social-links/" + addedSocialLink.Id, addedSocialLink); 89 | 90 | // Assert 91 | updateResponse.EnsureSuccessStatusCode(); 92 | postResponse.EnsureSuccessStatusCode(); 93 | } 94 | 95 | [Fact] 96 | public async Task Delete_With_Valid_Id_Returns_SuccessStatusCode() 97 | { 98 | // Arrange 99 | // Create a new social link 100 | await AuthenticateAsync(); 101 | SocialLinkDto dto = new SocialLinkDto 102 | { 103 | Name = "GitHub", 104 | URL = "https://github.com/SaraRasoulian" 105 | }; 106 | 107 | var postResponse = await _httpClient.PostAsJsonAsync("/api/social-links", dto); 108 | var addedSocialLink = await postResponse.Content.ReadFromJsonAsync(); 109 | 110 | // Act 111 | var deleteResponse = await _httpClient.DeleteAsync("/api/social-links/" + addedSocialLink.Id); 112 | 113 | // Assert 114 | postResponse.EnsureSuccessStatusCode(); 115 | deleteResponse.EnsureSuccessStatusCode(); 116 | 117 | var getResponse = await _httpClient.GetAsync("api/social-links/" + addedSocialLink.Id); 118 | getResponse.Should().HaveStatusCode(HttpStatusCode.NoContent); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /server/tests/WebApi.Tests.Integration/ControllersTests/ExperiencesControllerTests.cs: -------------------------------------------------------------------------------- 1 | namespace WebApi.Tests.Integration.ControllersTests; 2 | 3 | public class ExperiencesControllerTests : BaseControllerTest 4 | { 5 | public ExperiencesControllerTests(IntegrationTestWebApplicationFactory factory) : base(factory) 6 | { } 7 | 8 | [Fact] 9 | public async Task GetAll_Returns_SuccessStatusCode() 10 | { 11 | // Act 12 | var response = await _httpClient.GetAsync("api/experiences"); 13 | 14 | // Assert 15 | response.EnsureSuccessStatusCode(); 16 | } 17 | 18 | [Fact] 19 | public async Task GetById_Returns_SuccessStatusCode() 20 | { 21 | // Arrange 22 | await AuthenticateAsync(); 23 | 24 | // Get a random id 25 | Guid id = Guid.NewGuid(); 26 | 27 | // Act 28 | var response = await _httpClient.GetAsync("api/experiences/" + id); 29 | 30 | // Assert 31 | response.EnsureSuccessStatusCode(); 32 | response.Should().HaveStatusCode(HttpStatusCode.NoContent); 33 | } 34 | 35 | [Fact] 36 | public async Task Unauthorized_GetById_Returns_UnauthorizedStatusCode() 37 | { 38 | // Arrange 39 | Guid id = Guid.NewGuid(); 40 | 41 | // Act 42 | var response = await _httpClient.GetAsync("api/experiences/" + id); 43 | 44 | // Assert 45 | response.Should().HaveStatusCode(HttpStatusCode.Unauthorized); 46 | } 47 | 48 | [Fact] 49 | public async Task Post_Returns_OK_With_ExperienceDto() 50 | { 51 | // Arrange 52 | await AuthenticateAsync(); 53 | ExperienceDto dto = new ExperienceDto 54 | { 55 | CompanyName = "Google", 56 | StartYear = "2020", 57 | EndYear = "2022", 58 | Description = "Lorem ipsum is a placeholder text", 59 | }; 60 | 61 | // Act 62 | var response = await _httpClient.PostAsJsonAsync("/api/experiences", dto); 63 | 64 | // Assert 65 | response.EnsureSuccessStatusCode(); 66 | 67 | var responseDto = await response.Content.ReadFromJsonAsync(); 68 | dto.Should().BeEquivalentTo(responseDto, options => options.Excluding(x => x.Id)); 69 | } 70 | 71 | [Fact] 72 | public async void Put_Returns_SuccessStatusCode() 73 | { 74 | // Arrange 75 | // Create a new experience 76 | await AuthenticateAsync(); 77 | ExperienceDto dto = new ExperienceDto 78 | { 79 | CompanyName = "Google", 80 | StartYear = "2020", 81 | EndYear = "2022", 82 | Description = "Lorem ipsum is a placeholder text", 83 | }; 84 | 85 | var postResponse = await _httpClient.PostAsJsonAsync("/api/experiences", dto); 86 | var addedExperience = await postResponse.Content.ReadFromJsonAsync(); 87 | 88 | // Update the added experience 89 | addedExperience.CompanyName = "Apple"; 90 | 91 | // Act 92 | var updateResponse = await _httpClient.PutAsJsonAsync("/api/experiences/" + addedExperience.Id, addedExperience); 93 | 94 | // Assert 95 | updateResponse.EnsureSuccessStatusCode(); 96 | postResponse.EnsureSuccessStatusCode(); 97 | } 98 | 99 | [Fact] 100 | public async Task Delete_With_Valid_Id_Returns_SuccessStatusCode() 101 | { 102 | // Arrange 103 | // Create a new experience 104 | await AuthenticateAsync(); 105 | ExperienceDto dto = new ExperienceDto 106 | { 107 | CompanyName = "Google", 108 | StartYear = "2020", 109 | EndYear = "2022", 110 | Description = "Lorem ipsum is a placeholder text", 111 | }; 112 | 113 | var postResponse = await _httpClient.PostAsJsonAsync("/api/experiences", dto); 114 | var addedExperience = await postResponse.Content.ReadFromJsonAsync(); 115 | 116 | // Act 117 | var deleteResponse = await _httpClient.DeleteAsync("/api/experiences/" + addedExperience.Id); 118 | 119 | // Assert 120 | postResponse.EnsureSuccessStatusCode(); 121 | deleteResponse.EnsureSuccessStatusCode(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /client/src/views/admin/socialLinks/DeleteSocial.vue: -------------------------------------------------------------------------------- 1 | 65 | 66 | -------------------------------------------------------------------------------- /client/src/views/admin/experiences/AddExperience.vue: -------------------------------------------------------------------------------- 1 | 76 | 77 | -------------------------------------------------------------------------------- /client/src/views/admin/profile/ViewProfile.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | 97 | -------------------------------------------------------------------------------- /client/src/views/admin/socialLinks/AddSocial.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | -------------------------------------------------------------------------------- /client/src/views/admin/educations/AddEducation.vue: -------------------------------------------------------------------------------- 1 | 81 | 82 | -------------------------------------------------------------------------------- /server/Portfolio.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 17 4 | VisualStudioVersion = 17.0.31903.59 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6ED356A7-8B47-4613-AD01-C85CF28491BD}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{664D406C-2F83-48F0-BFC3-408D5CB53C65}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi", "src\WebApi\WebApi.csproj", "{342F3934-A896-4715-ABC4-38FEDB6FC1B5}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Domain", "src\Domain\Domain.csproj", "{9F5BE64F-7BA3-40D2-9DEF-14F534E84989}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "src\Application\Application.csproj", "{5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Infrastructure", "src\Infrastructure\Infrastructure.csproj", "{FBBC90CE-43DC-423C-87B8-110C81C623C6}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AF1217B1-7DDB-4E5F-B558-F13D13F87A2B}" 19 | ProjectSection(SolutionItems) = preProject 20 | .dockerignore = .dockerignore 21 | Dockerfile = Dockerfile 22 | EndProjectSection 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WebApi.Tests.Integration", "tests\WebApi.Tests.Integration\WebApi.Tests.Integration.csproj", "{7262A137-9AB3-406B-AF5A-F809DB89DFB8}" 25 | EndProject 26 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application.Tests.Unit", "tests\Application.Tests.Unit\Application.Tests.Unit.csproj", "{FA92EE80-5E91-480C-8D9D-B17715FFBA0C}" 27 | EndProject 28 | Global 29 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 30 | Debug|Any CPU = Debug|Any CPU 31 | Release|Any CPU = Release|Any CPU 32 | EndGlobalSection 33 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 34 | {342F3934-A896-4715-ABC4-38FEDB6FC1B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {342F3934-A896-4715-ABC4-38FEDB6FC1B5}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {342F3934-A896-4715-ABC4-38FEDB6FC1B5}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {342F3934-A896-4715-ABC4-38FEDB6FC1B5}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {9F5BE64F-7BA3-40D2-9DEF-14F534E84989}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {9F5BE64F-7BA3-40D2-9DEF-14F534E84989}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {9F5BE64F-7BA3-40D2-9DEF-14F534E84989}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {9F5BE64F-7BA3-40D2-9DEF-14F534E84989}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {FBBC90CE-43DC-423C-87B8-110C81C623C6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {FBBC90CE-43DC-423C-87B8-110C81C623C6}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {FBBC90CE-43DC-423C-87B8-110C81C623C6}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {FBBC90CE-43DC-423C-87B8-110C81C623C6}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {7262A137-9AB3-406B-AF5A-F809DB89DFB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {7262A137-9AB3-406B-AF5A-F809DB89DFB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {7262A137-9AB3-406B-AF5A-F809DB89DFB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {7262A137-9AB3-406B-AF5A-F809DB89DFB8}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {FA92EE80-5E91-480C-8D9D-B17715FFBA0C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {FA92EE80-5E91-480C-8D9D-B17715FFBA0C}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {FA92EE80-5E91-480C-8D9D-B17715FFBA0C}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {FA92EE80-5E91-480C-8D9D-B17715FFBA0C}.Release|Any CPU.Build.0 = Release|Any CPU 58 | EndGlobalSection 59 | GlobalSection(SolutionProperties) = preSolution 60 | HideSolutionNode = FALSE 61 | EndGlobalSection 62 | GlobalSection(NestedProjects) = preSolution 63 | {342F3934-A896-4715-ABC4-38FEDB6FC1B5} = {6ED356A7-8B47-4613-AD01-C85CF28491BD} 64 | {9F5BE64F-7BA3-40D2-9DEF-14F534E84989} = {6ED356A7-8B47-4613-AD01-C85CF28491BD} 65 | {5895FEF3-F864-43DC-AAD5-CFB5D4DFDA13} = {6ED356A7-8B47-4613-AD01-C85CF28491BD} 66 | {FBBC90CE-43DC-423C-87B8-110C81C623C6} = {6ED356A7-8B47-4613-AD01-C85CF28491BD} 67 | {7262A137-9AB3-406B-AF5A-F809DB89DFB8} = {664D406C-2F83-48F0-BFC3-408D5CB53C65} 68 | {FA92EE80-5E91-480C-8D9D-B17715FFBA0C} = {664D406C-2F83-48F0-BFC3-408D5CB53C65} 69 | EndGlobalSection 70 | GlobalSection(ExtensibilityGlobals) = postSolution 71 | SolutionGuid = {3CB609D9-5D54-4C11-A371-DAAC8B74E430} 72 | EndGlobalSection 73 | EndGlobal 74 | -------------------------------------------------------------------------------- /client/src/views/admin/ChangePassword.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/EducationServiceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Tests; 2 | 3 | public class EducationServiceTests 4 | { 5 | private readonly IUnitOfWork unitOfWork; 6 | private readonly IEducationRepository educationRepository; 7 | 8 | public EducationServiceTests() 9 | { 10 | unitOfWork = A.Fake(); 11 | educationRepository = A.Fake(); 12 | A.CallTo(() => unitOfWork.Education).Returns(educationRepository); 13 | } 14 | 15 | [Fact] 16 | public async Task GetAll_With_Data_Returns_List_Of_EducationDto() 17 | { 18 | // Arrange 19 | var educationData = new List(); 20 | A.CallTo(() => educationRepository.GetAll()).Returns(educationData); 21 | var educationService = new EducationService(unitOfWork); 22 | 23 | // Act 24 | var result = await educationService.GetAll(); 25 | 26 | // Assert 27 | Assert.NotNull(result); 28 | Assert.IsAssignableFrom>(result); 29 | Assert.Equal(educationData.Count, result.Count()); 30 | } 31 | 32 | [Fact] 33 | public async Task GetById_With_Data_Returns_EducationDto() 34 | { 35 | // Arrange 36 | var educationId = Guid.NewGuid(); 37 | var educationData = A.Dummy(); 38 | A.CallTo(() => educationRepository.GetById(educationId)).Returns(educationData); 39 | var educationService = new EducationService(unitOfWork); 40 | 41 | // Act 42 | var result = await educationService.GetById(educationId); 43 | 44 | // Assert 45 | Assert.NotNull(result); 46 | Assert.IsType(result); 47 | } 48 | 49 | [Fact] 50 | public async Task Add_Returns_Added_EducationDto() 51 | { 52 | // Arrange 53 | var educationDto = A.Dummy(); 54 | var addedEducation = educationDto.Adapt(); 55 | addedEducation.Id = Guid.NewGuid(); 56 | A.CallTo(() => educationRepository.Add(A._)).Returns(addedEducation); 57 | 58 | var educationService = new EducationService(unitOfWork); 59 | 60 | // Act 61 | var result = await educationService.Add(educationDto); 62 | 63 | // Assert 64 | Assert.NotNull(result); 65 | Assert.IsType(result); 66 | Assert.Equal(addedEducation.Id, result.Id); 67 | } 68 | 69 | [Fact] 70 | public async Task Update_For_Successful_Update_Returns_True() 71 | { 72 | // Arrange 73 | var existingId = Guid.NewGuid(); 74 | var existingEducation = A.Dummy(); 75 | existingEducation.Id = existingId; 76 | A.CallTo(() => educationRepository.GetById(existingId)).Returns(existingEducation); 77 | 78 | var updatedEducationDto = A.Dummy(); 79 | updatedEducationDto.Id = existingId; 80 | var educationService = new EducationService(unitOfWork); 81 | 82 | // Act 83 | var result = await educationService.Update(existingId, updatedEducationDto); 84 | 85 | // Assert 86 | Assert.True(result); 87 | A.CallTo(() => unitOfWork.Education.Update(A.That.Matches(e => e.Id == existingId))).MustHaveHappenedOnceExactly(); 88 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 89 | } 90 | 91 | [Fact] 92 | public async Task Update_For_Id_Mismatch_Returns_False() 93 | { 94 | // Arrange 95 | var educationService = new EducationService(unitOfWork); 96 | var invalidEducationDto = A.Dummy(); 97 | var existingId = Guid.NewGuid(); 98 | 99 | // Act 100 | var result = await educationService.Update(existingId, invalidEducationDto); 101 | 102 | // Assert 103 | Assert.False(result); 104 | A.CallTo(() => unitOfWork.Education.GetById(A._)).MustNotHaveHappened(); 105 | A.CallTo(() => unitOfWork.Education.Update(A._)).MustNotHaveHappened(); 106 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustNotHaveHappened(); 107 | } 108 | 109 | [Fact] 110 | public async Task Delete_With_Existing_Id_Returns_True() 111 | { 112 | // Arrange 113 | var existingId = Guid.NewGuid(); 114 | var existingEducation = A.Dummy(); 115 | A.CallTo(() => educationRepository.GetById(existingId)).Returns(existingEducation); 116 | 117 | var educationService = new EducationService(unitOfWork); 118 | 119 | // Act 120 | var result = await educationService.Delete(existingId); 121 | 122 | // Assert 123 | Assert.True(result); 124 | A.CallTo(() => unitOfWork.Education.Delete(existingEducation)).MustHaveHappenedOnceExactly(); 125 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/ExperienceServiceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Tests; 2 | 3 | public class ExperienceServiceTests 4 | { 5 | private readonly IUnitOfWork unitOfWork; 6 | private readonly IExperienceRepository educationRepository; 7 | 8 | public ExperienceServiceTests() 9 | { 10 | unitOfWork = A.Fake(); 11 | educationRepository = A.Fake(); 12 | A.CallTo(() => unitOfWork.Experience).Returns(educationRepository); 13 | } 14 | 15 | [Fact] 16 | public async Task GetAll_With_Data_Returns_List_Of_ExperienceDto() 17 | { 18 | // Arrange 19 | var educationData = new List(); 20 | A.CallTo(() => educationRepository.GetAll()).Returns(educationData); 21 | var educationService = new ExperienceService(unitOfWork); 22 | 23 | // Act 24 | var result = await educationService.GetAll(); 25 | 26 | // Assert 27 | Assert.NotNull(result); 28 | Assert.IsAssignableFrom>(result); 29 | Assert.Equal(educationData.Count, result.Count()); 30 | } 31 | 32 | [Fact] 33 | public async Task GetById_With_Data_Returns_ExperienceDto() 34 | { 35 | // Arrange 36 | var educationId = Guid.NewGuid(); 37 | var educationData = A.Dummy(); 38 | A.CallTo(() => educationRepository.GetById(educationId)).Returns(educationData); 39 | var educationService = new ExperienceService(unitOfWork); 40 | 41 | // Act 42 | var result = await educationService.GetById(educationId); 43 | 44 | // Assert 45 | Assert.NotNull(result); 46 | Assert.IsType(result); 47 | } 48 | 49 | [Fact] 50 | public async Task Add_Returns_Added_ExperienceDto() 51 | { 52 | // Arrange 53 | var educationDto = A.Dummy(); 54 | var addedExperience = educationDto.Adapt(); 55 | addedExperience.Id = Guid.NewGuid(); 56 | A.CallTo(() => educationRepository.Add(A._)).Returns(addedExperience); 57 | 58 | var educationService = new ExperienceService(unitOfWork); 59 | 60 | // Act 61 | var result = await educationService.Add(educationDto); 62 | 63 | // Assert 64 | Assert.NotNull(result); 65 | Assert.IsType(result); 66 | Assert.Equal(addedExperience.Id, result.Id); 67 | } 68 | 69 | [Fact] 70 | public async Task Update_For_Successful_Update_Returns_True() 71 | { 72 | // Arrange 73 | var existingId = Guid.NewGuid(); 74 | var existingExperience = A.Dummy(); 75 | existingExperience.Id = existingId; 76 | A.CallTo(() => educationRepository.GetById(existingId)).Returns(existingExperience); 77 | 78 | var updatedExperienceDto = A.Dummy(); 79 | updatedExperienceDto.Id = existingId; 80 | var educationService = new ExperienceService(unitOfWork); 81 | 82 | // Act 83 | var result = await educationService.Update(existingId, updatedExperienceDto); 84 | 85 | // Assert 86 | Assert.True(result); 87 | A.CallTo(() => unitOfWork.Experience.Update(A.That.Matches(e => e.Id == existingId))).MustHaveHappenedOnceExactly(); 88 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 89 | } 90 | 91 | [Fact] 92 | public async Task Update_For_Id_Mismatch_Returns_False() 93 | { 94 | // Arrange 95 | var educationService = new ExperienceService(unitOfWork); 96 | var invalidExperienceDto = A.Dummy(); 97 | var existingId = Guid.NewGuid(); 98 | 99 | // Act 100 | var result = await educationService.Update(existingId, invalidExperienceDto); 101 | 102 | // Assert 103 | Assert.False(result); 104 | A.CallTo(() => unitOfWork.Experience.GetById(A._)).MustNotHaveHappened(); 105 | A.CallTo(() => unitOfWork.Experience.Update(A._)).MustNotHaveHappened(); 106 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustNotHaveHappened(); 107 | } 108 | 109 | [Fact] 110 | public async Task Delete_With_Existing_Id_Returns_True() 111 | { 112 | // Arrange 113 | var existingId = Guid.NewGuid(); 114 | var existingExperience = A.Dummy(); 115 | A.CallTo(() => educationRepository.GetById(existingId)).Returns(existingExperience); 116 | 117 | var educationService = new ExperienceService(unitOfWork); 118 | 119 | // Act 120 | var result = await educationService.Delete(existingId); 121 | 122 | // Assert 123 | Assert.True(result); 124 | A.CallTo(() => unitOfWork.Experience.Delete(existingExperience)).MustHaveHappenedOnceExactly(); 125 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /server/tests/Application.Tests.Unit/SocialLinkServiceTests.cs: -------------------------------------------------------------------------------- 1 | namespace Application.Tests; 2 | 3 | public class SocialLinkServiceTests 4 | { 5 | private readonly IUnitOfWork unitOfWork; 6 | private readonly ISocialLinkRepository socialLinkRepository; 7 | 8 | public SocialLinkServiceTests() 9 | { 10 | unitOfWork = A.Fake(); 11 | socialLinkRepository = A.Fake(); 12 | A.CallTo(() => unitOfWork.SocialLink).Returns(socialLinkRepository); 13 | } 14 | 15 | [Fact] 16 | public async Task GetAll_With_Data_Returns_List_Of_SocialLinkDto() 17 | { 18 | // Arrange 19 | var SocialLinkData = new List(); 20 | A.CallTo(() => socialLinkRepository.GetAll()).Returns(SocialLinkData); 21 | var SocialLinkService = new SocialLinkService(unitOfWork); 22 | 23 | // Act 24 | var result = await SocialLinkService.GetAll(); 25 | 26 | // Assert 27 | Assert.NotNull(result); 28 | Assert.IsAssignableFrom>(result); 29 | Assert.Equal(SocialLinkData.Count, result.Count()); 30 | } 31 | 32 | [Fact] 33 | public async Task GetById_With_Data_Returns_SocialLinkDto() 34 | { 35 | // Arrange 36 | var SocialLinkId = Guid.NewGuid(); 37 | var SocialLinkData = A.Dummy(); 38 | A.CallTo(() => socialLinkRepository.GetById(SocialLinkId)).Returns(SocialLinkData); 39 | var SocialLinkService = new SocialLinkService(unitOfWork); 40 | 41 | // Act 42 | var result = await SocialLinkService.GetById(SocialLinkId); 43 | 44 | // Assert 45 | Assert.NotNull(result); 46 | Assert.IsType(result); 47 | } 48 | 49 | [Fact] 50 | public async Task Add_Returns_Added_SocialLinkDto() 51 | { 52 | // Arrange 53 | var SocialLinkDto = A.Dummy(); 54 | var addedSocialLink = SocialLinkDto.Adapt(); 55 | addedSocialLink.Id = Guid.NewGuid(); 56 | A.CallTo(() => socialLinkRepository.Add(A._)).Returns(addedSocialLink); 57 | 58 | var SocialLinkService = new SocialLinkService(unitOfWork); 59 | 60 | // Act 61 | var result = await SocialLinkService.Add(SocialLinkDto); 62 | 63 | // Assert 64 | Assert.NotNull(result); 65 | Assert.IsType(result); 66 | Assert.Equal(addedSocialLink.Id, result.Id); 67 | } 68 | 69 | [Fact] 70 | public async Task Update_For_Successful_Update_Returns_True() 71 | { 72 | // Arrange 73 | var existingId = Guid.NewGuid(); 74 | var existingSocialLink = A.Dummy(); 75 | existingSocialLink.Id = existingId; 76 | A.CallTo(() => socialLinkRepository.GetById(existingId)).Returns(existingSocialLink); 77 | 78 | var updatedSocialLinkDto = A.Dummy(); 79 | updatedSocialLinkDto.Id = existingId; 80 | var SocialLinkService = new SocialLinkService(unitOfWork); 81 | 82 | // Act 83 | var result = await SocialLinkService.Update(existingId, updatedSocialLinkDto); 84 | 85 | // Assert 86 | Assert.True(result); 87 | A.CallTo(() => unitOfWork.SocialLink.Update(A.That.Matches(e => e.Id == existingId))).MustHaveHappenedOnceExactly(); 88 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 89 | } 90 | 91 | [Fact] 92 | public async Task Update_For_Id_Mismatch_Returns_False() 93 | { 94 | // Arrange 95 | var SocialLinkService = new SocialLinkService(unitOfWork); 96 | var invalidSocialLinkDto = A.Dummy(); 97 | var existingId = Guid.NewGuid(); 98 | 99 | // Act 100 | var result = await SocialLinkService.Update(existingId, invalidSocialLinkDto); 101 | 102 | // Assert 103 | Assert.False(result); 104 | A.CallTo(() => unitOfWork.SocialLink.GetById(A._)).MustNotHaveHappened(); 105 | A.CallTo(() => unitOfWork.SocialLink.Update(A._)).MustNotHaveHappened(); 106 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustNotHaveHappened(); 107 | } 108 | 109 | [Fact] 110 | public async Task Delete_With_Existing_Id_Returns_True() 111 | { 112 | // Arrange 113 | var existingId = Guid.NewGuid(); 114 | var existingSocialLink = A.Dummy(); 115 | A.CallTo(() => socialLinkRepository.GetById(existingId)).Returns(existingSocialLink); 116 | 117 | var SocialLinkService = new SocialLinkService(unitOfWork); 118 | 119 | // Act 120 | var result = await SocialLinkService.Delete(existingId); 121 | 122 | // Assert 123 | Assert.True(result); 124 | A.CallTo(() => unitOfWork.SocialLink.Delete(existingSocialLink)).MustHaveHappenedOnceExactly(); 125 | A.CallTo(() => unitOfWork.SaveChangesAsync(default)).MustHaveHappenedOnceExactly(); 126 | } 127 | } 128 | --------------------------------------------------------------------------------