(feat): blog server side mock

This commit is contained in:
Maksym Sadovnychyy 2022-05-22 20:54:12 +02:00
parent 28d115af0e
commit 49c262d5be
55 changed files with 1225 additions and 131 deletions

View File

@ -5,6 +5,7 @@
"dependencies": {
"bootstrap": "5.1.3",
"classnames": "^2.3.1",
"dayjs": "^1.11.2",
"history": "5.3.0",
"merge": "^2.1.1",
"react": "18.0.0",

View File

@ -0,0 +1,14 @@
import dayjs from 'dayjs'
import 'dayjs/locale/it'
import 'dayjs/locale/en'
import 'dayjs/locale/ru'
const dateFormat = (date: string): string => {
return dayjs(date)
.locale('en')
.format("MMMM YYYY, dddd")
}
export {
dateFormat
}

View File

@ -1,5 +1,7 @@
import { dateFormat } from './dateFormat'
import { getKeyValue } from './getKeyValue'
export {
getKeyValue
getKeyValue,
dateFormat
}

View File

@ -0,0 +1,3 @@
import { IBlogItemModel, ICategoryModel, IFetchResult } from "./models"
const apiUrl = 'https://localhost:59018/api/Blog'

View File

@ -0,0 +1,53 @@
import { IBlogItemsPaginationModel, IBlogItemModel, ICategoryModel, IFetchResult } from "./models"
const apiUrl = 'https://localhost:59018/api/Blogs'
export interface IGetBlogsRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IGetBlogsResponse {
featuredBlog?: IBlogItemModel,
blogItemsPagination?: IBlogItemsPaginationModel,
categories?: ICategoryModel []
}
const GetBlogs = async (props?: IGetBlogsRequest): Promise<IGetBlogsResponse> => {
const url = new URL(apiUrl)
if(props) {
Object.keys(props).forEach(key => {
if (typeof(props[key]) !== undefined) {
url.searchParams.append(key, props[key] as string)
}
})
}
const requestParams = {
method: 'GET',
headers: { 'accept': 'application/json', 'content-type': 'application/json' },
}
console.log(`invoke:`, url.toString())
const fetchData = await fetch(url.toString(), requestParams)
.then(async fetchData => {
return {
status: fetchData.status,
text: await fetchData.text()
}
})
.catch(err => {
console.log(err)
})
return JSON.parse((fetchData as IFetchResult).text) as IGetBlogsResponse
}
export {
GetBlogs
}

View File

@ -0,0 +1,62 @@
export interface IFetchResult {
status: number,
text: string
}
export interface IImageModel {
src: string,
alt: string
}
export interface IAuthorModel {
id: string,
image?: IImageModel,
nickName: string
}
interface IPostItemModel {
id: string,
slug: string,
badge?: string,
image?: IImageModel,
title: string,
shortText: string,
text: string,
author: IAuthorModel,
created: string,
tags: string[]
}
export interface IBlogItemModel extends IPostItemModel {
readTime: number,
likes: number
}
export interface IShopItemModel extends IPostItemModel {
images?: IImageModel [],
sku: string,
rating?: number,
price: number,
newPrice?: number,
quantity?: number
}
interface IPostPaginationModel {
currentPage: number,
totalPages: number
}
export interface IBlogItemsPaginationModel extends IPostPaginationModel {
items: IBlogItemModel []
}
export interface IShopItemsPaginationModel extends IPostPaginationModel {
items: IShopItemModel []
}
export interface ICategoryModel {
id: string,
text: string
}

View File

@ -0,0 +1,13 @@
const apiUrl = 'https://localhost:59018/api/Blogs'
export interface IGetBlogsRequest {
[key: string]: string | undefined
category?: string,
searchText?: string,
currentPage?: string,
itemsPerPage?: string
}
export interface IBlogsResponse {
}

View File

@ -1,23 +1,87 @@
import React from 'react'
import React, { FC, useEffect, useState } from 'react'
import { Link } from 'react-router-dom'
import { Card, CardBody, CardHeader, CardImg, Col, Container, Row } from 'reactstrap'
import { Card, CardBody, CardFooter, CardHeader, CardImg, Col, Container, Row } from 'reactstrap'
import { dateFormat } from '../../../functions'
import { GetBlogs, IGetBlogsResponse } from '../../../httpQueries/blogs'
import { IBlogItemModel, IBlogItemsPaginationModel } from '../../../httpQueries/models'
import { Categories, Empty, Search } from '../SideWidgets'
const FeaturedBlog: FC<IBlogItemModel> = (props) => {
const { id, slug, badge, image, title, shortText, author, created, readTime, likes, tags } = props
return <Card className="mb-4 shadow border-0">
<CardImg top {...image} />
<CardBody className="p-4">
<div className="badge bg-primary bg-gradient rounded-pill mb-2">{badge}</div>
<Link className="text-decoration-none link-dark stretched-link" to={`blog/item/${slug}`}>
<h5 className="card-title mb-3">{title}</h5>
</Link>
<p className="card-text mb-0" dangerouslySetInnerHTML={{ __html: shortText }}></p>
</CardBody>
<CardFooter className="p-4 pt-0 bg-transparent border-top-0">
<div className="d-flex align-items-end justify-content-between">
<div className="d-flex align-items-center">
<img className="rounded-circle me-3" {...author.image} />
<div className="small">
<div className="fw-bold">{author.nickName}</div>
<div className="text-muted">{dateFormat(created)} &middot; Time to read: {readTime} min</div>
</div>
</div>
</div>
</CardFooter>
</Card>
}
const BlogPagination: FC<IBlogItemsPaginationModel> = (props) => {
const { items } = props
return <>
{items.map((item, index) => <Col key={index} className="lg-6">
<Card className="mb-4">
<CardImg top {...item.image} />
<CardBody>
<div className="small text-muted">{item.created}</div>
<h2 className="card-title h4">{item.title}</h2>
<p className="card-text">{item.shortText}</p>
<Link to={`${item.slug}`} className="btn btn-primary">Read more </Link>
</CardBody>
</Card>
</Col>)}
<nav aria-label="Pagination">
<hr className="my-0" />
<ul className="pagination justify-content-center my-4">
<li className="page-item disabled"><a className="page-link" href="#" aria-disabled="true">Newer</a></li>
<li className="page-item active" aria-current="page"><a className="page-link" href="#!">1</a></li>
<li className="page-item"><a className="page-link" href="#!">2</a></li>
<li className="page-item"><a className="page-link" href="#!">3</a></li>
<li className="page-item disabled"><a className="page-link" href="#!">...</a></li>
<li className="page-item"><a className="page-link" href="#!">15</a></li>
<li className="page-item"><a className="page-link" href="#!">Older</a></li>
</ul>
</nav>
</>
}
const BlogCatalog = () => {
const items = [
{
slug: "1"
},
{
slug: "2"
},
{
slug: "2"
},
{
slugd: "2"
}
]
const [state, setState] = useState<IGetBlogsResponse>()
useEffect(() => {
GetBlogs().then(response => {
setState(response)
})
}, [])
return <>
<header className="py-5 bg-light border-bottom mb-4">
@ -32,57 +96,16 @@ const BlogCatalog = () => {
<Container fluid>
<Row>
<Col>
<Card className="mb-4">
<Link to={`blog/item/featured`}>
<CardImg top src="https://dummyimage.com/850x350/dee2e6/6c757d.jpg" alt="..." />
</Link>
<CardBody>
<div className="small text-muted">January 1, 2022</div>
<h2 className="card-title">Featured Post Title</h2>
<p className="card-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reiciendis aliquid atque, nulla? Quos cum ex quis soluta, a laboriosam. Dicta expedita corporis animi vero voluptate voluptatibus possimus, veniam magni quis!</p>
<a className="btn btn-primary" href="#!">Read more </a>
</CardBody>
</Card>
{state?.featuredBlog ? <FeaturedBlog {...state.featuredBlog} /> : ''}
<Row>
{items.map((item, index) => <Col key={index} className="lg-6">
<Card className="mb-4">
<Link to={`${item.slug}`}>
<CardImg top src="https://dummyimage.com/850x350/dee2e6/6c757d.jpg" alt="..." />
</Link>
<CardBody>
<div className="small text-muted">January 1, 2022</div>
<h2 className="card-title h4">Post Title</h2>
<p className="card-text">Lorem ipsum dolor sit amet, consectetur adipisicing elit. Reiciendis aliquid atque, nulla.</p>
<a className="btn btn-primary" href="#!">Read more </a>
</CardBody>
</Card>
</Col>)}
<nav aria-label="Pagination">
<hr className="my-0" />
<ul className="pagination justify-content-center my-4">
<li className="page-item disabled"><a className="page-link" href="#" aria-disabled="true">Newer</a></li>
<li className="page-item active" aria-current="page"><a className="page-link" href="#!">1</a></li>
<li className="page-item"><a className="page-link" href="#!">2</a></li>
<li className="page-item"><a className="page-link" href="#!">3</a></li>
<li className="page-item disabled"><a className="page-link" href="#!">...</a></li>
<li className="page-item"><a className="page-link" href="#!">15</a></li>
<li className="page-item"><a className="page-link" href="#!">Older</a></li>
</ul>
</nav>
{state?.blogItemsPagination ? <BlogPagination {...state.blogItemsPagination} /> : '' }
</Row>
</Col>
<Col lg="4">
<Search />
<Categories />
{state?.categories ? <Categories {...{
categories: state.categories
}} /> : '' }
<Empty/>
</Col>
</Row>

View File

@ -1,5 +1,6 @@
import React from 'react'
import { Card, CardBody, CardHeader, Col, Row } from 'reactstrap'
import { ICategoryModel } from '../../../httpQueries/models'
const Search = () => {
return <Card className="mb-4">
@ -13,23 +14,35 @@ const Search = () => {
</Card>
}
const Categories = () => {
export interface ICategories {
categories?: ICategoryModel []
}
const Categories = (props: ICategories) => {
const { categories } = props
if(!categories) {
return <></>
}
const middleIndex = Math.ceil(categories.length / 2)
const firstHalf = categories.splice(0, middleIndex)
const secondHalf = categories.splice(-middleIndex)
return <Card className="mb-4">
<CardHeader>Categories</CardHeader>
<CardBody>
<Row>
<Col sm="6">
<ul className="list-unstyled mb-0">
<li><a href="#!">Web Design</a></li>
<li><a href="#!">HTML</a></li>
<li><a href="#!">Freebies</a></li>
{firstHalf.map((item, index) => <li key={index}><a href="#!">{item.text}</a></li>)}
</ul>
</Col>
<Col sm="6">
<ul className="list-unstyled mb-0">
<li><a href="#!">JavaScript</a></li>
<li><a href="#!">CSS</a></li>
<li><a href="#!">Tutorials</a></li>
{secondHalf.map((item, index) => <li key={index}><a href="#!">{item.text}</a></li>)}
</ul>
</Col>
</Row>

View File

@ -3265,6 +3265,11 @@ data-urls@^2.0.0:
whatwg-mimetype "^2.3.0"
whatwg-url "^8.0.0"
dayjs@^1.11.2:
version "1.11.2"
resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.2.tgz#fa0f5223ef0d6724b3d8327134890cfe3d72fbe5"
integrity sha512-F4LXf1OeU9hrSYRPTTj/6FbO4HTjPKXvEIC1P2kcnFurViINCVk3ZV0xAS3XVx9MkMsXbbqlK6hjseaYbgKEHw==
debug@2.6.9, debug@^2.6.0, debug@^2.6.9:
version "2.6.9"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"

25
webapi/.dockerignore Normal file
View File

@ -0,0 +1,25 @@
**/.classpath
**/.dockerignore
**/.env
**/.git
**/.gitignore
**/.project
**/.settings
**/.toolstarget
**/.vs
**/.vscode
**/*.*proj.user
**/*.dbmdl
**/*.jfm
**/azds.yaml
**/bin
**/charts
**/docker-compose*
**/Dockerfile*
**/node_modules
**/npm-debug.log
**/obj
**/secrets.dev.yaml
**/values.dev.yaml
LICENSE
README.md

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Abstractions.DomainObjects {
public class Category {
public Guid Id { get; set; }
public string Text { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Abstractions.DomainObjects {
public abstract class DomainObject {
}
}

View File

@ -0,0 +1,35 @@
using Core.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Abstractions.DomainObjects {
public abstract class PostItem : DomainObject {
public Guid Id { get; set; }
/// <summary>
/// Author / Owner
/// </summary>
public Guid UserId { get; set; }
public string Title { get; set; }
public string Text { get; set; }
public string Badge { get; set; }
public List<string> Tags { get; set; }
public List<string> Categories { get; set; }
public DateTime Created { get; set; }
/// <summary>
/// Edit dateTime, and Author
/// </summary>
public Dictionary<DateTime, Guid> Edited { get; set; }
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Abstractions.Models {
public abstract class RequestModel {
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Abstractions.Models {
public abstract class ResponseModel {
}
}

9
webapi/Core/Core.csproj Normal file
View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,9 @@
using Core.Abstractions.DomainObjects;
namespace Core.DomainObjects {
internal class BlogItem : PostItem {
public int Likes { get; set; }
public int ReadingTime { get; set; }
}
}

View File

@ -0,0 +1,17 @@
using Core.Abstractions;
using Core.Abstractions.DomainObjects;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.DomainObjects {
internal class ShopItem : PostItem {
public string Sku { get; set; }
public int Rating { get; set; }
public int Price { get; set; }
public int NewPrice { get; set; }
public int Quantity { get; set; }
}
}

View File

@ -0,0 +1,29 @@
using Core.Abstractions;
using Core.Abstractions.DomainObjects;
using Core.Entities;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.DomainObjects {
public class User : DomainObject {
public Guid Id { get; set; }
public string NickName { get; set; }
public string Hash { get; set; }
public string Salt { get; set; }
public string Name { get; set; }
public string LastName { get; set; }
public Contact Email { get; set; }
public Contact Mobile { get; set; }
public Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
}

View File

@ -0,0 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Entities {
public class Address {
public string Street { get; set; }
public string City { get; set; }
public string PostCode { get; set; }
public string Country { get; set; }
}
}

View File

@ -0,0 +1,12 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Entities {
public class Contact {
public string Value { get; set; }
public bool IsConfirmed { get; set; }
}
}

View File

@ -0,0 +1,15 @@
using Core.Abstractions;
using Core.Abstractions.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Models {
public class PaginationModel<T> {
public int TotalPages { get; set; }
public int CurrentPage { get; set; }
public List<T> Items { get; set; }
}
}

View File

@ -0,0 +1,14 @@
namespace DataProviders;
public interface IBlogDataProvider { }
public class BlogDataProvider : IBlogDataProvider {
private readonly IDataProvidersConfiguration _configuration;
public BlogDataProvider(IDataProvidersConfiguration configuration) {
_configuration = configuration;
}
}

View File

@ -0,0 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,15 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders {
public interface IDataProvidersConfiguration {
public Database Database { get; set; }
}
public class Database {
public string ConnectionString { get; set; }
}
}

View File

@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders {
public interface IShopDataProvider {
}
public class ShopDataProvider : IShopDataProvider {
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace DataProviders {
public class SiteSettingsDataProvider {
}
}

View File

@ -0,0 +1,40 @@
using System.Text;
using System.Security.Cryptography;
using Microsoft.AspNetCore.Cryptography.KeyDerivation;
namespace Services {
public interface IHashService {
(string, string) CreateSaltedHash(string value);
bool ValidateHash(string value, string salt, string hash);
}
public class HashService : IHashService {
private string CreateSalt() {
byte[] randomBytes = new byte[128 / 8];
using (var generator = RandomNumberGenerator.Create()) {
generator.GetBytes(randomBytes);
return Convert.ToBase64String(randomBytes);
}
}
private string CreateHash(string value, string salt) {
var valueBytes = KeyDerivation.Pbkdf2(
password: value,
salt: Encoding.UTF8.GetBytes(salt),
prf: KeyDerivationPrf.HMACSHA512,
iterationCount: 10000,
numBytesRequested: 256 / 8);
return Convert.ToBase64String(valueBytes);
}
public (string, string) CreateSaltedHash(string value) {
var salt = CreateSalt();
var hash = CreateHash(value, salt);
return (salt, hash);
}
public bool ValidateHash(string value, string salt, string hash) => CreateHash(value, salt) == hash;
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Cryptography.KeyDerivation" Version="6.0.5" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,7 @@
using System.IO;
namespace Services {
public interface IJwtServiceSettings {
string Secret { get; set; }
}
}

View File

@ -0,0 +1,51 @@
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using Microsoft.IdentityModel.Tokens;
namespace Services {
public interface IJwtService {
string CreateJwtToken(IEnumerable<string> issuer, DateTime expires, string userId, string userEmail, string userName, IEnumerable<string> userRoles);
JwtSecurityToken ReadJwtToken(string token);
}
public class JwtService : IJwtService {
private readonly JwtSecurityTokenHandler _tokenHandler;
private readonly IJwtServiceSettings _serviceSettings;
public JwtService(IJwtServiceSettings serviceSettings) {
_serviceSettings = serviceSettings;
_tokenHandler = new JwtSecurityTokenHandler();
}
public string CreateJwtToken(IEnumerable<string> issuer, DateTime expires, string userId, string userEmail, string userName, IEnumerable<string> userRoles) {
var key = Convert.FromBase64String(_serviceSettings.Secret);
// add roles to claims identity from database
var claims = new List<Claim>() {
new Claim(ClaimTypes.Actor, userId),
new Claim(ClaimTypes.Email, userEmail),
new Claim(ClaimTypes.NameIdentifier, userName),
// new Claim(ClaimTypes.Webpage, issuer)
};
foreach (var role in userRoles)
claims.Add(new Claim(ClaimTypes.Role, role));
foreach (var iss in issuer)
claims.Add(new Claim(ClaimTypes.Webpage, iss));
var token = _tokenHandler.CreateToken(new SecurityTokenDescriptor {
IssuedAt = DateTime.UtcNow,
Subject = new ClaimsIdentity(claims),
Expires = expires,
SigningCredentials = new SigningCredentials(new SymmetricSecurityKey(key), SecurityAlgorithms.HmacSha512Signature),
});
return _tokenHandler.WriteToken(token);
}
public JwtSecurityToken ReadJwtToken(string token) => _tokenHandler.ReadJwtToken(token);
}
}

View File

@ -0,0 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.IdentityModel.Tokens.Jwt" Version="6.18.0" />
</ItemGroup>
</Project>

View File

@ -5,6 +5,18 @@ VisualStudioVersion = 17.0.31912.275
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "WeatherForecast", "WeatherForecast\WeatherForecast.csproj", "{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Services", "Services", "{113EE574-E047-4727-AA36-841F845504D5}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HashService", "Services\HashService\HashService.csproj", "{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JWTService", "Services\JWTService\JWTService.csproj", "{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "Core\Core.csproj", "{BCDED8EB-97B0-4067-BB0A-23F94D1A1288}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataProviders", "DataProviders\DataProviders.csproj", "{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}"
EndProject
Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@ -15,10 +27,34 @@ Global
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{065AC673-3C4D-4C08-B1A9-3C3A1467B3A7}.Release|Any CPU.Build.0 = Release|Any CPU
{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B}.Release|Any CPU.Build.0 = Release|Any CPU
{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7}.Release|Any CPU.Build.0 = Release|Any CPU
{BCDED8EB-97B0-4067-BB0A-23F94D1A1288}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BCDED8EB-97B0-4067-BB0A-23F94D1A1288}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BCDED8EB-97B0-4067-BB0A-23F94D1A1288}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BCDED8EB-97B0-4067-BB0A-23F94D1A1288}.Release|Any CPU.Build.0 = Release|Any CPU
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
{13EDFAD4-5D8B-4879-96F7-D896265FB0DC}.Release|Any CPU.Build.0 = Release|Any CPU
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1FE09D24-5FC7-4EDD-AC19-C06DB9C035DB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{B8F84A37-B54B-4606-9BC3-6FEB96A5A34B} = {113EE574-E047-4727-AA36-841F845504D5}
{B717D8BD-BCCA-4515-9A62-CA3BE802D0F7} = {113EE574-E047-4727-AA36-841F845504D5}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E2805D02-2425-424C-921D-D97341B76F73}
EndGlobalSection

View File

@ -0,0 +1,5 @@
namespace WeatherForecast {
public class Configuration {
public string Secret { get; set; }
}
}

View File

@ -0,0 +1,107 @@
using Microsoft.AspNetCore.Mvc;
using Core.Models;
using WeatherForecast.Models;
using Microsoft.AspNetCore.Authorization;
using Core.Abstractions.Models;
namespace WeatherForecast.Controllers;
#region Input models
public class GetBlogsResponse : ResponseModel {
public BlogItemModel FeaturedBlog { get; set; }
public List<CategoryModel> Categories { get; set; }
public PaginationModel<BlogItemModel> BlogItemsPagination { get; set; }
}
#endregion
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class BlogsController : ControllerBase {
private readonly ILogger<LoginController> _logger;
public BlogsController(ILogger<LoginController> logger) {
_logger = logger;
}
/// <summary>
///
/// </summary>
/// <param name="currentPage"></param>
/// <param name="itemsPerPage"></param>
/// <param name="category"></param>
/// <param name="searchText"></param>
/// <returns></returns>
[HttpGet]
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) {
var blogItemModel = new BlogItemModel {
Id = Guid.NewGuid(),
Slug = "blog-post-title",
Image = new ImageModel { Src = "https://dummyimage.com/850x350/dee2e6/6c757d.jpg", Alt = "..." },
Badge = "news",
Title = "Blog post title",
ShortText = "Lorem ipsum, dolor sit amet consectetur adipisicing elit. Eaque fugit ratione dicta mollitia. Officiis ad...",
Author = new AuthorModel {
Id = Guid.NewGuid(),
Image = new ImageModel { Src = "https://dummyimage.com/40x40/ced4da/6c757d", Alt = "..." },
NickName = "Admin"
},
Created = DateTime.UtcNow,
ReadTime = 10,
Likes = 200,
Tags = new List<string> { "react", "redux", "webapi" }
};
var blogModels = new List<BlogItemModel>();
for (int i = 0; i < itemsPerPage; i++) {
blogModels.Add(blogItemModel);
}
var blogResponse = new GetBlogsResponse {
FeaturedBlog = blogItemModel,
BlogItemsPagination = new PaginationModel<BlogItemModel> {
CurrentPage = currentPage,
TotalPages = 100,
Items = blogModels
},
Categories = new List<CategoryModel> {
new CategoryModel {
Id = Guid.NewGuid(),
Text = "Web Design"
},
new CategoryModel {
Id = Guid.NewGuid(),
Text = "Html"
},
new CategoryModel {
Id = Guid.NewGuid(),
Text = "Freebies"
},
new CategoryModel {
Id = Guid.NewGuid(),
Text = "Javascript"
},
new CategoryModel {
Id = Guid.NewGuid(),
Text = "CSS"
},
new CategoryModel {
Id = Guid.NewGuid(),
Text = "Tutorials"
}
}
};
return Ok(blogResponse);
}
}

View File

@ -0,0 +1,31 @@
using Core.Abstractions.Models;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
public class PostLoginRequest : RequestModel {
public string Username { get; set; }
public string Password { get; set; }
}
[ApiController]
[Route("[controller]")]
public class LoginController : ControllerBase {
private readonly ILogger<LoginController> _logger;
public LoginController(ILogger<LoginController> logger) {
_logger = logger;
}
[HttpPost(Name = "Login")]
public IActionResult Post([FromBody] PostLoginRequest requestBody) {
return BadRequest();
}
}

View File

@ -0,0 +1,47 @@
using Core.Abstractions.Models;
using Core.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
#region Response models
public class GetShopCatalogResponse : ResponseModel {
public PaginationModel<ShopItemModel> ShopItemsPagination { get; set; }
}
#endregion
[AllowAnonymous]
[ApiController]
[Route("api/[controller]")]
public class ShopCatalog : ControllerBase {
private readonly ILogger<LoginController> _logger;
public ShopCatalog(ILogger<LoginController> logger) {
_logger = logger;
}
/// <summary>
///
/// </summary>
/// <param name="category"></param>
/// <param name="searchText"></param>
/// <param name="currentPage"></param>
/// <param name="itemsPerPage"></param>
/// <returns></returns>
[HttpGet]
public IActionResult Get([FromQuery] Guid? category, [FromQuery] string? searchText, [FromQuery] int currentPage = 1, [FromQuery] int itemsPerPage = 4) {
var shopItemModel = new ShopItemModel {
};
return Ok();
}
}

View File

@ -1,8 +1,22 @@
using Microsoft.AspNetCore.Cors;
using Core.Abstractions.Models;
using Microsoft.AspNetCore.Mvc;
using WeatherForecast.Models;
namespace WeatherForecast.Controllers;
#region Response models
public class GetWeatherForecastResponse : ResponseModel {
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}
#endregion
[ApiController]
[Route("[controller]")]
@ -21,10 +35,9 @@ public class WeatherForecastController : ControllerBase
}
[HttpGet(Name = "GetWeatherForecast")]
public IEnumerable<WeatherForecast> Get()
public IEnumerable<GetWeatherForecastResponse> Get()
{
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
return Enumerable.Range(1, 5).Select(index => new GetWeatherForecastResponse {
Date = DateTime.Now.AddDays(index),
TemperatureC = Random.Shared.Next(-20, 55),
Summary = Summaries[Random.Shared.Next(Summaries.Length)]

View File

@ -0,0 +1,23 @@
#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging.
FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base
WORKDIR /app
EXPOSE 80
EXPOSE 443
FROM mcr.microsoft.com/dotnet/sdk:6.0 AS build
WORKDIR /src
COPY ["WeatherForecast/WeatherForecast.csproj", "WeatherForecast/"]
COPY ["Core/Core.csproj", "Core/"]
RUN dotnet restore "WeatherForecast/WeatherForecast.csproj"
COPY . .
WORKDIR "/src/WeatherForecast"
RUN dotnet build "WeatherForecast.csproj" -c Release -o /app/build
FROM build AS publish
RUN dotnet publish "WeatherForecast.csproj" -c Release -o /app/publish
FROM base AS final
WORKDIR /app
COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "WeatherForecast.dll"]

View File

@ -0,0 +1,7 @@
namespace WeatherForecast.Models {
public class AuthorModel {
public Guid Id { get; set; }
public ImageModel Image { get; set; }
public string NickName { get; set; }
}
}

View File

@ -0,0 +1,11 @@
namespace WeatherForecast.Models {
public class BlogItemModel : PostItemModel {
public int ReadTime { get; set; }
public int Likes { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models {
public class CategoryModel {
public Guid Id { get; set; }
public string Text { get; set; }
}
}

View File

@ -0,0 +1,6 @@
namespace WeatherForecast.Models {
public class ImageModel {
public string Src { get; set; }
public string Alt { get; set; }
}
}

View File

@ -0,0 +1,23 @@
namespace WeatherForecast.Models {
public class PostItemModel {
public Guid Id { get; set; }
public string Slug { get; set; }
public ImageModel Image { get; set; }
public string Badge { get; set; }
public string Title { get; set; }
public string ShortText { get; set; }
public string Text { get; set; }
public AuthorModel Author { get; set; }
public DateTime Created { get; set; }
public List<string> Tags { get; set; }
}
}

View File

@ -0,0 +1,10 @@
namespace WeatherForecast.Models {
public class ShopItemModel : PostItemModel {
public List<ImageModel> Images { get; set; }
public string Sku { get; set; }
public int Rating { get; set; }
public int Price { get; set; }
public int NewPrice { get; set; }
public int Quantity { get; set; }
}
}

View File

@ -1,41 +1,13 @@
var builder = WebApplication.CreateBuilder(args);
namespace WeatherForecast {
public class Program {
public static void Main(string[] args) {
CreateHostBuilder(args).Build().Run();
}
var MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
builder.Services.AddCors(options =>
{
options.AddPolicy(name: MyAllowSpecificOrigins,
builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
});
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder => {
webBuilder.UseStartup<Startup>();
});
}
}
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthorization();
app.MapControllers();
app.Run();

View File

@ -1,4 +1,4 @@
{
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
@ -11,13 +11,13 @@
"profiles": {
"WeatherForecast": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"launchUrl": "swagger",
"applicationUrl": "https://localhost:7151;http://localhost:5133",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"applicationUrl": "https://localhost:7151;http://localhost:5133",
"dotnetRunMessages": true
},
"IIS Express": {
"commandName": "IISExpress",
@ -26,6 +26,13 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"Docker": {
"commandName": "Docker",
"launchBrowser": true,
"launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger",
"publishAllPorts": true,
"useSSL": true
}
}
}
}

View File

@ -0,0 +1,166 @@
using System.Reflection;
using Microsoft.OpenApi.Models;
using Microsoft.IdentityModel.Tokens;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace WeatherForecast {
public class Startup {
public IConfiguration _configuration { get; }
string MyAllowSpecificOrigins = "_myAllowSpecificOrigins";
public Startup(IConfiguration configuration) {
_configuration = configuration;
}
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services) {
string serverHostName = Environment.MachineName;
// configure strongly typed settings objects
var appSettingsSection = _configuration.GetSection("Configuration");
services.Configure<Configuration>(appSettingsSection);
var appSettings = appSettingsSection.Get<Configuration>();
services.AddCors(options => {
options.AddPolicy(MyAllowSpecificOrigins,
builder => {
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader();
});
});
services.AddControllers();
// configure jwt authentication
services.AddAuthentication(options => {
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options => {
options.RequireHttpsMetadata = false;
options.SaveToken = true;
options.TokenValidationParameters = new TokenValidationParameters {
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Convert.FromBase64String(appSettings.Secret)),
ValidateIssuer = false,
ValidateAudience = false
};
});
// https://docs.microsoft.com/en-us/aspnet/core/fundamentals/http-context?view=aspnetcore-3.1#use-httpcontext-from-custom-components
services.AddHttpContextAccessor();
#region Swagger
services.ConfigureSwaggerGen(options => {
// your custom configuration goes here
// UseFullTypeNameInSchemaIds replacement for .NET Core
options.CustomSchemaIds(x => x.FullName);
});
// Register the Swagger generator, defining 1 or more Swagger documents
services.AddSwaggerGen(config => {
//c.SerializeAsV2 = true,
config.SwaggerDoc("v2", new OpenApiInfo {
Title = "MAKS-IT WEB API",
Version = "v2",
Description = "Site support webapi for blogs or e-commerce",
// TermsOfService = new Uri(""),
/*
Contact = new OpenApiContact
{
Name = "",
Email = "",
Url = new Uri(""),
},
*/
License = new OpenApiLicense {
Name = "Use under ISC",
Url = new Uri("https://opensource.org/licenses/ISC")
}
});
// Set the comments path for the Swagger JSON and UI.
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
config.IncludeXmlComments(xmlPath);
// https://stackoverflow.com/questions/56234504/bearer-authentication-in-swagger-ui-when-migrating-to-swashbuckle-aspnetcore-ve
config.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
Description = "JWT Authorization header using the Bearer scheme. \r\n\r\n Enter 'Bearer' [space] and then your token in the text input below.\r\n\r\nExample: \"Bearer 12345abcdef\"",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
config.AddSecurityRequirement(new OpenApiSecurityRequirement()
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
},
Scheme = "oauth2",
Name = "Bearer",
In = ParameterLocation.Header,
},
new List<string>()
}
});
// c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First()); //This line
});
#endregion
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory) {
if (env.IsDevelopment()) {
app.UseDeveloperExceptionPage();
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c => {
c.DefaultModelsExpandDepth(-1);
c.SwaggerEndpoint("/swagger/v2/swagger.json", "MAKS-IT WEB API v2");
// To serve the Swagger UI at the app's root (http://localhost:<port>/), set the RoutePrefix property to an empty string
c.RoutePrefix = "swagger";
});
}
else {
// app.UseExceptionHandler("/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
// app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseRouting();
// UseCors must be called before UseResponseCaching
app.UseCors(MyAllowSpecificOrigins);
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints => {
endpoints.MapControllers();
});
}
}
}

View File

@ -1,12 +0,0 @@
namespace WeatherForecast;
public class WeatherForecast
{
public DateTime Date { get; set; }
public int TemperatureC { get; set; }
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
public string? Summary { get; set; }
}

View File

@ -5,10 +5,20 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>WeatherForecast</RootNamespace>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<UserSecretsId>2ea970dd-e71a-4c8e-9ff6-2d1d3123d4df</UserSecretsId>
<DockerDefaultTargetOS>Linux</DockerDefaultTargetOS>
<DockerComposeProjectPath>..\docker-compose.dcproj</DockerComposeProjectPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="6.0.5" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.14.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Core\Core.csproj" />
</ItemGroup>
</Project>

View File

@ -5,5 +5,8 @@
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
"AllowedHosts": "*",
"Configuration": {
"Secret": "TUlJQ1d3SUJBQUtCZ0djczU2dnIzTWRwa0VYczYvYjIyemxMWlhSaFdrSWtyN0dqUHB4ZkNpQk9FU2Q3L2VxcA=="
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" Sdk="Microsoft.Docker.Sdk">
<PropertyGroup Label="Globals">
<ProjectVersion>2.1</ProjectVersion>
<DockerTargetOS>Linux</DockerTargetOS>
<ProjectGuid>1fe09d24-5fc7-4edd-ac19-c06db9c035db</ProjectGuid>
<DockerLaunchAction>LaunchBrowser</DockerLaunchAction>
<DockerServiceUrl>{Scheme}://localhost:{ServicePort}/swagger</DockerServiceUrl>
<DockerServiceName>weatherforecast</DockerServiceName>
</PropertyGroup>
<ItemGroup>
<None Include="docker-compose.override.yml">
<DependentUpon>docker-compose.yml</DependentUpon>
</None>
<None Include="docker-compose.yml" />
<None Include=".dockerignore" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,13 @@
version: '3.4'
services:
weatherforecast:
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ASPNETCORE_URLS=https://+:443;http://+:80
ports:
- "80"
- "443"
volumes:
- ${APPDATA}/Microsoft/UserSecrets:/root/.microsoft/usersecrets:ro
- ${APPDATA}/ASP.NET/Https:/root/.aspnet/https:ro

View File

@ -0,0 +1,8 @@
version: '3.4'
services:
weatherforecast:
image: ${DOCKER_REGISTRY-}weatherforecast
build:
context: .
dockerfile: WeatherForecast/Dockerfile