diff --git a/README.md b/README.md index 8b12214..634edac 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,35 @@ This is my personal website build in [Hugo](https://gohugo.io) and uses [terminal](https://github.com/panr/hugo-theme-terminal) theme -- Run +### Run ```bash hugo server -D -``` \ No newline at end of file +``` + + +### Add image + +With link and subtitle + +```md + + + +RSA Encryption (lecture-rsa-dotnet-javascript.vercel.app) +

RSA Encryption (lecture-rsa-dotnet-javascript.vercel.app)

+ +
+ + +``` + +With subtitle + +```md + +Microservice architecture with Ocelot and Keycloak +

Microservice architecture with Ocelot and Keycloak

+ +``` + diff --git a/config.toml b/config.toml index 4def4ca..5280016 100644 --- a/config.toml +++ b/config.toml @@ -1,11 +1,13 @@ baseURL = 'https://berkselvi.dev' languageCode = 'en-us' defaultContentLanguage = 'en' -title = "Berk Selvi | Web Dev" theme = "terminal" -paginate = 5 +paginate = 10 googleAnalytics = 'G-W0L24EMSQ5' +[markup.goldmark.renderer] + unsafe = true + [params] # dir name of your main content (default is `content/posts`). # the list of set content will show up on your index page (baseurl). @@ -36,7 +38,7 @@ googleAnalytics = 'G-W0L24EMSQ5' showLastUpdated = false # set a custom favicon (default is a `themeColor` square) - # favicon = "favicon.ico" + favicon = "/icon/favicon.ico" # Provide a string as a prefix for the last update date. By default, it looks like this: 2020-xx-xx [Updated: 2020-xx-xx] :: Author # updatedDatePrefix = "Updated" @@ -65,7 +67,7 @@ googleAnalytics = 'G-W0L24EMSQ5' [languages] [languages.en] disabled = false - languageName = "English" + languageName = "english" languageCode = 'en-US' contentDir = 'content/english' weight = 1 @@ -85,10 +87,17 @@ googleAnalytics = 'G-W0L24EMSQ5' [languages.en.params.logo] logoText = "Berk Selvi" logoHomeLink = "/" - + + [languages.en.menu] + [[languages.en.menu.main]] + identifier = "home" + name = "home" + url = "/" + weight = 1 + [languages.tr] disabled = false - languageName = "Türkçe" + languageName = "türkçe" languageCode = 'tr-TR' contentDir = 'content/turkish' weight = 2 @@ -107,4 +116,12 @@ googleAnalytics = 'G-W0L24EMSQ5' [languages.tr.params.logo] logoText = "Berk Selvi" - logoHomeLink = "/" \ No newline at end of file + logoHomeLink = "/" + + [languages.tr.menu] + [[languages.tr.menu.main]] + identifier = "home" + name = "home" + url = "/" + weight = 1 + diff --git a/content/english/_index.html b/content/english/_index.html index e24cffc..8ba03e7 100644 --- a/content/english/_index.html +++ b/content/english/_index.html @@ -1,59 +1,57 @@ +++ -title = "Berk Selvi - Software developer" -date = "2023-01-21T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -description = "Hello! My name is Berk Selvi and I am a full-stack web developer specializing in backend and interested in data science. I am currently studying Computer Science at Namık Kemal University and I am also developing side projects that solves real-world problems, and I love doing that!" -layout = "index" -framed = true +++ -
-
-

Hello world!

-
+
+
+
+

Hey, It's Berk Selvi

+

.NET Developer, blogger and builder.

+
+ +
+
+ Berk Selvi +
- - - -
- - GitHub - - - Medium - - - Twitter - - - Instagram - -
\ No newline at end of file +
+

Who am I?

+

+ Berk Selvi is a software developer who works with .Net & Java for backend and React & Vue for frontend technologies in the field of full stack web development, has an Azure certificate, contributes to the community through blog posts. He is willing to learn new technologies and is happy to develop products as part of teamwork, and is interested in DevOps. He is currently part of Doğuş Technology's insurance team. +

+
diff --git a/content/english/posts/5-core-philosophies-that-every-developer-should-have.md b/content/english/posts/5-core-philosophies-that-every-developer-should-have.md index 9ce68fe..06a295e 100644 --- a/content/english/posts/5-core-philosophies-that-every-developer-should-have.md +++ b/content/english/posts/5-core-philosophies-that-every-developer-should-have.md @@ -3,10 +3,8 @@ title = "5 Core Philosophies That Every Developer Should Have" date = "2022-03-09T11:01:10+03:00" author = "Berk Selvi" authorTwitter = "berkslv" #do not include @ -tags = ["learning", "study", "knowledge"] keywords = ["learning", "study", "knowledge"] description = "When you learn how to learn, you unlock almost all the possibilities in the world with knowledge." -canonicalUrl = "https://medium.com/@berkselvi/5-core-philosophies-that-every-developer-should-have-45d1c12a838b" showFullContent = false readingTime = true +++ @@ -14,6 +12,7 @@ readingTime = true I myself think that software is a philosophy of life before it is a job, and I take great pleasure in approaching most of the problems in my life as a software developer and solving them that way. In this article, I will talk about the cornerstones of this philosophy. ## Table of Contents + - Be a responsible person - Don’t ruin anything in your code - Don’t be a perfectionist @@ -88,7 +87,6 @@ A good idea is an orphan when you don’t have effective communication. As devel --- - ## Conclusion -This information that I am trying to convey to you has emerged as a result of the blending of my own experiences and the first part of the Pragmatic Programmer book. If there is anything you would like to add or correct, mention it in the comments and you should subscribe to the email if you find this information valuable. Thanks so much for your time! 🥳 \ No newline at end of file +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md b/content/english/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md new file mode 100644 index 0000000..aed3bf8 --- /dev/null +++ b/content/english/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md @@ -0,0 +1,454 @@ ++++ +title = "Achieving Zero Downtime: Azure App Service Deployment using Azure DevOps and Deployment Slots" +date = "2023-05-21T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["ci/cd","azure devops","azure app service",".NET","azure"] +description = "When deploying our application, which runs as a single instance in Azure App Service, using Azure DevOps pipelines, there will most likely be a few seconds of downtime. Because the application…" +showFullContent = false +readingTime = true ++++ + +Hello! When deploying our application, which runs as a single instance in Azure App Service, using Azure DevOps pipelines, there will most likely be a few seconds of downtime. Because the application running as a single instance will need to be restarted to update it with a new version. In a worst case scenario, if an error occurs in the transition of our application versions, downtime will be extended due to rollback. + +We can use the deployment slots feature to solve this problem specifically for the app service. With this feature, 2 different instances, usually staging and production applications, are running in a single app service plan as a separate instances, and when switching to production, the swap operation is managed by the app service and no downtime is experienced. + +In this post, we will create a very simple Dotnet Web API project, host it in the Azure DevOps repository, deploy it to Azure App Service by creating a CI/CD pipeline with the Azure DevOps pipeline, and perform zero downtime deployment with the deployment slots feature. We will complete the entire process in 5 steps as follows. Let’s begin. + +1. Creating the application +2. Azure DevOps Repo +3. Azure DevOps build pipeline +4. Azure App service +5. Zero downtime testing + +## Creating the application + + +We create a simple Dotnet Web API project to deploy. To do this, we create our project with the following commands. + +```bash +mkdir backend +cd ./backend +dotnet new sln -n Slot +dotnet new webapi -n Slot.API +dotnet sln add ./Slot.API/ +``` + +We update the _profiles.http_ property in the _properties/launchSetting.json_ file as follows. Here we only updated the _applicationUrl_ and _launchBrowser_ properties. + +```json +// ... + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5050", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, +// ... +``` + +By making the following update on Program.cs, we will test that the application is accessible and can meet the requests made to the /health endpoint. If you are using a database in your application, you can also test whether there is a problem accessing the database with [AddDbContextCheck](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkcorehealthchecksbuilderextensions.adddbcontextcheck?view=dotnet-plat-ext-8.0) method. + +```cs +var builder = WebApplication.CreateBuilder(args); + +// ... + +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// ... + +// app.UseHttpsRedirection(); + +app.MapControllers(); + +app.UseHealthChecks("/health"); + +app.Run(); +``` + +That’s it for our application! Now we can access the localhost:5050/health endpoint by running + +```bash +cd ./Slot.API +dotnet run +curl -4 http://localhost:5050/health +# Healthy +``` + +Later, we will use Docker to deploy our application, so we put our Dockerfile file in the same directory as our sln file. + +```Dockerfile +# Build Stage +FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS base +WORKDIR /app +EXPOSE 8080 + + +# Publish Stage +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build +COPY ["Slot.API/Slot.API.csproj", "Slot.API/"] +RUN dotnet restore "Slot.API/Slot.API.csproj" +COPY . . +WORKDIR "/Slot.API" +RUN dotnet build "Slot.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Slot.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENV ASPNETCORE_URLS=http://*:8080 +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "Slot.API.dll"] +``` + +We can use the following two commands to ensure that we can successfully build the image and run the container. + +```bash +docker build -t deployment-slots-demo . +docker run -it -p 80:8080 deployment-slots-demo -n deployment-slots-demo-container +``` + +## Verify that the application is up + + +After creating our application with the health check feature in the previous step, we can verify that the application is accessible by making a request to this endpoint every second. This way, we will verify that it is accessible when deployed to Azure. + +In this section, we will write a simple Node.js script to verify that the application can be accessed by making requests every second. We use the following commands for this. + +```bash +mkdir health-check +npm init -y +touch index.js +npm install node-fetch +``` + +We add the following script to the package.json file. + +```json +// ... +"scripts": { + "start": "node index.js" +}, +"type": "module", +// ... +``` + +We can create our index.js file as follows. With this code, we will make a request to the given url every 50 milisecond and write the response to the console. + +```js +import fetch from "node-fetch"; + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("http://localhost:5050/health"); + }, 50); + setInterval(() => { + check("http://localhost:5050/health"); + }, 50); +})(); +``` + +If you do not turn off the UseHttpsRedirection middleware in our API project, you may receive an invalid SSL certificate error. You can fix this as follows. + +```js +import fetch from "node-fetch"; +import https from "https"; + +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + agent: httpsAgent, + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("https://localhost:5051/health"); + }, 50); + setInterval(() => { + check("https://localhost:5051/health"); + }, 50); +})(); +``` + +## Azure DevOps Repo + +We log into our Azure DevOps account and create a new repo as follows. + +Azure deployment + +
+ +Azure deployment + +We will clone this repo to our computer, move the code that we write into this repo folder and push the codes to origin. + +Azure deployment + +```bash +git add . +git commit -m "inital commit" +git push origin +``` + +Azure deployment + +## Azure DevOps build pipeline + +On the Pipeline screen, we click on the New pipeline button from the top right. Then, we build the docker file and push it to Azure Container Registry by following the steps below. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +Now we check that our image deployment in Azure Container Registry is successful. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +As we can see, when the pipeline is run, the docker image has been successfully created in the Azure Container Registry and is waiting to be used. After these settings, a new docker image will be created by triggering every change made in the /backend directory on the main branch. + +## Azure App service + +Azure App service allows us to deploy web applications with security, load balancing, autoscaling managed by Azure itself. We can create a new app service via the Azure app services screen as follows. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +After creating the app service, you may need to update the Container Registry password from the configuration tab. + +## Azure DevOps release pipeline + +With the release pipeline, we deploy to the App service using the Docker image produced by the build pipeline and publish our application. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +By going into staging deployment, we get deployment for our staging version and restart our app service to ensure that the docker image change we made goes live. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +In our production step, we do not take a deployment to the app service, instead we perform the swap operation with staging and in this way, we start running our application running in staging in production without experiencing any downtime. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +From now on, any changes made to the main branch will first run the build pipeline (CI), then the deployment pipeline (CD), and deployment will be made to the staging environment. If you want to switch to Production, Production deployment must be triggered manually from the release screen. + +Azure deployment + +## Zero downtime testing + +We have successfully created a pipeline for our application, from now on we will need to test whether there is any downtime during deployment. For this, I update our _health-check/index.js_ file as follows and run the application with the npm run start command and trigger the pipeline. And then I complete the deployment process without receiving any error messages on the console, that is, without any downtime! + +```js +import fetch from "node-fetch"; + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("https://deployment-slot-demo.azurewebsites.net/health"); + }, 50); + setInterval(() => { + check("https://deployment-slot-demo-staging.azurewebsites.net/health"); + }, 50); +})(); +``` + +If we try a similar process in an app service deployment that does not implement deployment slots, we will receive an error as follows. + +```bash +2023-11-14T12:28:39.506Z, [some-url]/health error is request to [some-url]/health failed, reason: connect ETIMEDOUT 100.100.100.100:443 +``` + +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/background-jobs-and-hangife-in-net.md b/content/english/posts/background-jobs-and-hangife-in-net.md new file mode 100644 index 0000000..be36687 --- /dev/null +++ b/content/english/posts/background-jobs-and-hangife-in-net.md @@ -0,0 +1,398 @@ ++++ +title = "Background jobs and Hangfire in .Net" +date = "2024-09-17T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = [".NET","hangfire","background jobs"] +description = "Learn about managing background jobs in .Net with various methods like Task.Run(), Hosted Service, and Hangfire. This comprehensive blog post explores their pros, cons, and implementation details, with a focus on Hangfire..." +showFullContent = false +readingTime = true ++++ + + +When developing an application in the .Net ecosystem, when things get complicated, we may need some of our methods to go to multiple services, evaluate their responses, and report these results to different services and this takes long time! We do not want to waste resources by placing such long-running methods behind an endpoint and keeping our TCP connection open for the response from that HTTP request. We can call this usage on demand job because it will run when a request is made. We may also want it to run automatically at certain times of the day or week, without putting it behind an endpoint; in this case, we can call it a recurring job. In such cases, we use “background job” by starting our work on a different thread other than the main thread where the application runs. + +There are many different methods of managing background jobs in .Net. This will be a blog post where I will talk about them step by step, how they solve problems and what problems they cause us, and give in-depth usage examples in Hangfire, which is the package I like the most when dealing with background jobs. You can also access all the codes from the repo: + +[GitHub - berkslv/lecture-dotnet-background-jobs](https://github.com/berkslv/lecture-dotnet-background-jobs) + + +## Task.Run() + + +We can create a new thread and run a method on it with the Task.Run() method, which is the first solution that comes to my mind when I need a background job. I have a method that has dependencies on many external services that can take up to 5 minutes to complete, and I was calling this method with an HTTP request through a controller that I created, but answering this request was problematic because it is required a TCP connection to remain open for 5 minutes, so instead when I called the method, If started successfully, I had to notify the client that the process was started successfully with a 200 status code. + +```cs +// TestService.cs + +public class TestService : ITestService +{ + private readonly ILogger _logger; + public TestService(ILogger logger) + { + _logger = logger; + } + + public bool RunTests() + { + _logger.LogInformation($"{DateTime.Now} RunTests is started"); + + // ... + Thread.Sleep(5000); + // ... + + _logger.LogInformation($"{DateTime.Now} RunTests is finished"); + return true; + } +} +``` + +```cs +// Program.cs + +// ... + +builder.Services.AddTransient(); + +// ... +``` + +If I do not make the execution by calling with the Task.Run(), the request to my /job endpoint will be loading for 3 seconds in browser for this example. + +```cs +// JobController.cs + +[ApiController] +[Route("[controller]")] +public class JobController : ControllerBase +{ + private readonly ITestService _testService; + public JobController(ITestService testService) + { + _testService = testService; + } + + [HttpGet] + public IActionResult Get() + { + _testService.RunTests(); + + return Ok("Ok"); + } +} +``` + +If I use Task.Run instead as below, the controller will respond ok and my RunTests method will continue to run in the background. + +```cs +[HttpGet] +public IActionResult Get() +{ + Task.Run(() => { + _testService.RunTests(); + }); + + return Ok("Ok"); +} +``` + +### Pros + +- We can run a process in the background on demand. +- Does not require an additional package. + +### Cons + +- It does not support recurring in its current state, a special system must be developed. +- When Dependency Injection is used, since the injected interfaces will remain on the main thread, we may need to re-generate the required interface through the Service scope. +- What happens if an error is throwed while running the Method? + +## Hosted Service + +We had to develop our own system to manage recurring jobs, which we could not implement in our previous example, but with the Hosted service, we do not need to develop this management ourselves, instead we call our AddHostedService method in Program.cs as follows and inherit our TestService class from the BackgroundService class. In this example, our RunTests method will run every 10 seconds. This time interval is set from the ExecuteAsync method inherited from BackgroundService abstract class. + +```cs +// Program.cs + +// ... + +builder.Services.AddHostedService(); + +// ... +``` + +```cs +// TestService.cs + + +public class TestService : BackgroundService, ITestService +{ + private readonly ILogger _logger; + public TestService(ILogger logger) + { + _logger = logger; + } + + public bool RunTests(TestType testType) + { + var type = Enum.GetName(typeof(TestType), testType); + + _logger.LogInformation($"{DateTime.Now} {type} RunTests is started"); + + // ... + Thread.Sleep(5000); + // ... + + _logger.LogInformation($"{DateTime.Now} {type} RunTests is finished"); + + return true; + } + + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var timer = new PeriodicTimer(TimeSpan.FromSeconds(10)); + while (await timer.WaitForNextTickAsync(stoppingToken)) + { + RunTests(TestType.Recurring); + } + } +} +``` + +However, if we want to run our method on demand, we work on a new thread with the help of the Task.Run method, so we do not need to make any changes to JobController.cs. + +### Pros + +* Provides recurring job management +* No need to install additional packages + +### Cons + +* It does not have a system for on demand operation. +* What happens if an error is received while running the Method? + + +## Hangfire + + +Hangfire makes our job much easier to manage on demand and recurring jobs through a single system. With the Job Storage system, which is not available in the other two methods, if the application is not running at that moment but the cron job has expired, it automatically runs the relevant job. We can run specific jobs and delete that job with the ID information provided during job creation. Additionally, we can monitor currently running jobs via a dashboard at /hangfire. + +Hangfire dashboard +

Hangfire dashboard

+ +Hangfire officially supports the Sql Server database, but with an open source extension, frequently preferred databases such as Sqlite and Postgresql can also be used. In addition, Hangfire, with its paid version, also meets enterprise needs such as Redis database support and running batch jobs. + +[Hangfire – Background Jobs for .NET and .NET Core](https://www.hangfire.io/extensions.html) + +After this brief introduction to Hangfire, let’s briefly talk about how we can use Hangfire in our application and its capabilities. In our example, we will use Postgresql as Job storage, but you can choose any database you want from the link above. After installing the following packages in our application and setting up a Postgre SQL database with docker, let’s move on to our code. + +```bash +dotnet add package Hangfire +dotnet add package Hangfire.Core +dotnet add package Hangfire.PostgreSql +dotnet add package TimeZoneConverter + +docker run -d --name postgres_db -e POSTGRES_USER="root" -e POSTGRES_PASSWORD="1234" -e POSTGRES_DB="postgres" -v postgres_data:/var/lib/postgresql/data -p 5432:5432 postgres +``` + +First, we provide the connection string information for Postgresql in appsettings.json as follows. + +```json +"ConnectionStrings": { + "HangfireConnection": "Host=localhost;Port=5432;Password=1234;Username=root;Database=postgres;Pooling=true;Integrated Security=true;" +} +``` + +We make the relevant configurations in Program.cs as follows. + +With TZConvert.GetTimeZoneInfo method you get the neccessary time zone information from OS. This line of code is neccessary because your local machine, frontend application and Cloud machine may have different time zones. + +```cs +var builder = WebApplication.CreateBuilder(args); + +// ... + +builder.Services.AddHangfire(config => { + config + .UseSimpleAssemblyNameTypeSerializer() + .UseRecommendedSerializerSettings() + .UsePostgreSqlStorage(builder.Configuration.GetConnectionString("HangfireConnection")); + + var cronEveryMinute = "*/1 * * * *"; + var recurringJobOptions = new RecurringJobOptions + { + TimeZone = TZConvert.GetTimeZoneInfo("Etc/GMT+3") + }; + RecurringJob.AddOrUpdate("id-run-and-wait", x => x.RunTests(Guid.NewGuid(), TestType.Recurring, CancellationToken.None), cronEveryMinute, recurringJobOptions); +}); + +builder.Services.AddHangfireServer(); + +var app = builder.Build(); + +// ... + +app.UseHangfireDashboard(); +app.MapHangfireDashboard(); + +app.Run(); +``` + +To showcase Hangfire’s capabilities, we add a few more endpoints to our controller class. + +**/run,** we can start a job and that method returns us a job id + +**/stop,** we can stop the job related to the job id given to us by Hangfire. + +**/continue,** if many different jobs are to be run but they are dependent on each other, another job can be run after the parent job is finished with the given job id. + +**/reschedule,** the job’s working intervals can be dynamically adjusted by cron or TimeSpan. + +**/deschedule,** recurring jobs can be deleted by their unique id. + +**/trigger,** we can manually trigger a recurring job. + +```cs +// JobController.cs + + +[ApiController] +[Route("[controller]")] +public class JobController : ControllerBase +{ + [HttpGet("/run")] + public IActionResult Run() + { + var jobId = BackgroundJob.Enqueue(x => x.RunTests(Guid.NewGuid(), TestType.OnDemand, CancellationToken.None)); + + return Ok(jobId); + } + + [HttpGet("/stop")] + public IActionResult Stop(string jobId) + { + BackgroundJob.Delete(jobId); + + return Ok("Stopped"); + } + + [HttpGet("/continue")] + public IActionResult Continue(string jobId) + { + BackgroundJob.ContinueJobWith(jobId, x => x.RunTests(Guid.NewGuid(), TestType.OnDemand, CancellationToken.None)); + + return Ok("Continued"); + } + + [HttpGet("/reschedule")] + public IActionResult Reschedule(string cron) + { + var recurringJobOptions = new RecurringJobOptions + { + TimeZone = TZConvert.GetTimeZoneInfo("Etc/GMT+3") + }; + RecurringJob.AddOrUpdate("id-run-and-wait", x => x.RunTests(Guid.NewGuid(), TestType.Recurring, CancellationToken.None), cron, recurringJobOptions); + return Ok("Rescheduled"); + } + + [HttpGet("/deschedule")] + public IActionResult Deschedule(string id) + { + if (String.IsNullOrEmpty(id)) + { + id = "id-run-and-wait"; + } + + RecurringJob.RemoveIfExists(id); + return Ok("Descheduled"); + } + + [HttpGet("/trigger")] + public IActionResult Trigger(string id) + { + if (String.IsNullOrEmpty(id)) + { + id = "id-run-and-wait"; + } + + RecurringJob.TriggerJob(id); + return Ok("Triggered"); + } +} +``` + +Finally, error management, which is not in our toolkit previously with Task.Run and Hosted service but with Hangfire if an error occurs while running a method, Hangfire runs that method 10 more times with the same parameters at certain time intervals. As an example, we add a method called ThrowRandomly to our TestService class. With this method, we simply add a system that will throw an exception from the method that works with probability 1/2, but Hangfire will try to get successful results by re-running the methods that get errors for us. But errors that catches successfully cannot trigger the retry system. Therefore in the end of catch block we throw again. + +```cs +// TestService.cs + + +public class TestService : ITestService +{ + private readonly ILogger _logger; + public TestService(ILogger logger) + { + _logger = logger; + } + + public bool RunTests(Guid id, TestType testType, CancellationToken cancellationToken) + { + var type = Enum.GetName(typeof(TestType), testType); + + try + { + _logger.LogInformation($"{DateTime.Now} {type} RunTests is started. Id: {id}"); + + cancellationToken.ThrowIfCancellationRequested(); + // ... + Thread.Sleep(5000); + ThrowRandomly(); + // ... + + _logger.LogInformation($"{DateTime.Now} {type} RunTests is finished. Id: {id}"); + return true; + } + catch (OperationCanceledException exception) + { + _logger.LogError($"{DateTime.Now} {type} RunTests is failed. Exception: {exception.Message} Id: {id}"); + throw; + } + catch(Exception exception) + { + _logger.LogError($"{DateTime.Now} {type} RunTests is failed. Exception: {exception.Message} Id: {id}"); + throw; + } + } + + private void ThrowRandomly() + { + var random = new Random(); + var number = random.Next(1, 3); + + if (number == 2) + { + throw new Exception("Error is throwed!"); + } + } +} +``` + +Also Hangfire runs on a seperate service, this can be good or bad depending on your needs. When you need scaling you may seperate the application and hangfire servers and place them to different machines. + +### Pros + +* Can manage on demand and recurring jobs together with a powerful abstraction +* We can adjust cron job timing dynamically and its timing is very precise. +* We can monitor our employee and cron jobs with Dashborad. +* There is no imposed interface implementation or any other special implementation, we can only manage our jobs by using the methods provided by Hangfire. + +### Cons + +* External storage is required, works with SQL Server by default. + + +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/dockerize-a-react-app-with-nginx-and-react-router-dom.md b/content/english/posts/how-to-deploy-a-react-app-with-nginx-using-docker-with-react-router-dom.md similarity index 93% rename from content/english/posts/dockerize-a-react-app-with-nginx-and-react-router-dom.md rename to content/english/posts/how-to-deploy-a-react-app-with-nginx-using-docker-with-react-router-dom.md index b6fc814..cc4ca13 100644 --- a/content/english/posts/dockerize-a-react-app-with-nginx-and-react-router-dom.md +++ b/content/english/posts/how-to-deploy-a-react-app-with-nginx-using-docker-with-react-router-dom.md @@ -1,11 +1,10 @@ +++ title = "How to Deploy a React App with Nginx using Docker with react-router-dom" -date = "2023-01-21T11:01:10+03:00" +date = "2023-01-21T00:00:00+03:00" author = "Berk Selvi" authorTwitter = "berkslv" #do not include @ -tags = ["react","nginx","docker","dockerfile","docker-compose", "deployment"] -keywords = ["react","nginx","docker","dockerfile","docker-compose", "deployment"] -description = "Learn how to deploy a React app with Nginx using Docker in this step-by-step tutorial. This tutorial covers everything from creating a Dockerfile for your React app to using Docker Compose to streamline the deployment process. By the end of this tutorial, you'll have a solid understanding of how to deploy a React app with Nginx using Docker and Docker Compose." +keywords = ["react","nginx","docker","deployment"] +description = "Learn how to deploy a React app with Nginx using Docker in this step-by-step tutorial. This tutorial covers everything from creating a Dockerfile for your React app to using Docker Compose to ..." showFullContent = false readingTime = true +++ @@ -134,3 +133,8 @@ This command tells Docker Compose to build the my-react-app service using the Do Congratulations! You have successfully deployed a React app with Nginx using Docker and Docker Compose. You can now easily deploy your React apps to any server or hosting platform that supports Docker. +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/how-to-download-website-with-all-assets-and-sub-pages-with-one-line-command.md b/content/english/posts/how-to-download-website-with-all-assets-and-sub-pages-with-one-line-command.md deleted file mode 100644 index ca4f21a..0000000 --- a/content/english/posts/how-to-download-website-with-all-assets-and-sub-pages-with-one-line-command.md +++ /dev/null @@ -1,27 +0,0 @@ -+++ -title = "How to download website with all assets and sub pagess, with one line command!" -date = "2023-01-21T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -tags = ["web scaraping", "wget", "website content", "website"] -keywords = ["web scaraping", "wget", "website content", "website"] -description = "There may be times when you need to download an entire website, including all of its pages, images, and styles. In this blog post, we'll take a look at a popular tool that you can use to download an entire website: wget." -showFullContent = false -readingTime = true -+++ - -There may be times when you need to download an entire website, including all of its pages, images, and styles. This can be useful for archiving a website, creating a local copy for offline viewing, or for creating a backup. In this blog post, we'll take a look at a popular tool that you can use to download an entire website: **wget**. - -wget is a command-line tool that can be used to download files from the internet. It's a powerful tool that can be used to download a single file, a group of files, or an entire website. To download an entire website using wget, you can use the following command: - -```bash - -wget -r -np -k -p -E -nc [website URL] - -``` - -This command tells wget to download the website recursively (-r), not to ascend to the parent directory (-np), to make links to files on the same server point to local files (-k), to download all necessary assets (-p), to adjust the extension of the files it saves (-E) and to not download files if they are already present in your local copy (-nc). - -It's important to note that not all websites allow for their content to be downloaded in this way. Some websites may have terms of service that prohibit the use of automated tools to download their content. Additionally, it's also important to respect website's copyright and legal restrictions while downloading any website. - -In conclusion, wget are two powerful tool that can be used to download an entire website, including all of its pages, images, and styles. However, it's important to ensure that you have the legal right to download the website, and to respect the website's terms of service. Remember that downloading a website should be done only for legitimate purposes and not for any malicious intent. \ No newline at end of file diff --git a/content/english/posts/how-to-learn-something-very-well.md b/content/english/posts/how-to-learn-something-very-well.md index 3766f73..343c5ee 100644 --- a/content/english/posts/how-to-learn-something-very-well.md +++ b/content/english/posts/how-to-learn-something-very-well.md @@ -3,7 +3,6 @@ title = "How to learn something very well" date = "2022-03-09T10:01:10+03:00" author = "Berk Selvi" authorTwitter = "berkslv" #do not include @ -tags = ["development", "software"] keywords = ["development", "software"] description = "Basic 5 skills that every software developer should have before writing code." showFullContent = false @@ -28,6 +27,7 @@ Don’t go ahead without testing yourself, you might think you learned that way, ## Things to do + Recalling from memory is a much more wonderful method compared to methods such as repeated reading. When reading articles, pause at the beginning of sentences or paragraphs and ask, “What is the main idea? What is the message you want to give?” asking questions like this is a great practice for our callback method. In this way, mental connections related to the subjects learned will be strengthened. End-of-chapter questions and self-created questions are great learning methods. It can be used here in quizzes and exams, Using Quizzes as a learning tool may be unorthodox in terms of danger, but works very well for recall and your answers to the questions are important. You can reveal your shortcomings in this way and completing these areas will move you forward. @@ -48,4 +48,4 @@ Blend recall and detailing. After a topic or class, ask yourself, “How did it ## Conclusion -This information that I am trying to convey to you has emerged as a result of the blending of my own experiences and Make it Stick book. If there is anything you would like to add or correct, mention it in the comments and you should subscribe to the email if you find this information valuable. Thanks so much for your time! 🥳 \ No newline at end of file +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/how-to-secure-dotnet-vue-application-with-keycloak.md b/content/english/posts/how-to-secure-dotnet-vue-application-with-keycloak.md new file mode 100644 index 0000000..e21ba58 --- /dev/null +++ b/content/english/posts/how-to-secure-dotnet-vue-application-with-keycloak.md @@ -0,0 +1,304 @@ ++++ +title = "How to Secure Dotnet & Vue.js Application with Keycloak" +date = "2023-08-21T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["keycloak",".NET","vue","microservices"] +description = "Discover how to secure dotnet and Vue apps with Keycloak. This guide covers Docker setup, realm and client configuration, securing Vue.js apps, creating a Dotnet WebAPI, consuming the API from Vue, and more..." +showFullContent = false +readingTime = true ++++ + + +In this post, I will talk about how we can secure our dotnet and vue applications using keycloak. + +First, we run our keycloak application using docker, if you publish this application using Azure or a similar cloud provider, you can easily use it in your production applications. + +```bash +docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:22.0.1 start-dev +``` + +After Keycloak is running, we create realm for our application from the left side after logging in from localhost:8080/admin with username: admin and password: admin. Since master realm is assigned to keycloak’s own use, we need to work on another realm. + +Create realm +

Create realm

+ +Create realm confirmation +

Create realm confirmation

+ + +After creating the realm, we create a client called vue from the client section. Our redirect urls are the url of our Vue app. You may need to make additional updates for production. + +We can create users under dotnet-vue realm from the Users menu to be used in our vue application. + +If we want to create a user for our application, we can enable our users to register with keycloak by turning on this option in realm settings. + +## Vue + +After completing these configurations, we are done with the keycloak admin panel, when an unauthorized request comes to our vue application, we have to redirect it to our keycloak login page. For this, we use the medium article [Secure Vue.js app with Keycloak](https://medium.com/keycloak/secure-vue-js-app-with-keycloak-94814181e344) + +We can create our Vue application using vite as follows, you can choose Vue and Javascript from the questions asked and continue. + +```bash +npm create vite@latest vue +cd vue +npm install +npm run dev +``` + +Then we update the main.js content as follows. This script simply redirects the unauthorized requests to login and updates the token with the refresh token if the token expires with onTokenExpired. After this process, if we run the application with npm run dev and go to http://localhost:5173, the keycloak login url will be redirected. If we log in with the user information we have created, we will be logged into our system. + +```js + +import { createApp } from "vue"; +import "./style.css"; +import App from "./App.vue"; +import Keycloak from "keycloak-js"; + +let initOptions = { + url: "http://localhost:8080", + realm: "dotnet-vue", + clientId: "vue", + onLoad: "login-required", +}; + +let keycloak = new Keycloak(initOptions); + +keycloak + .init({ onLoad: initOptions.onLoad }) + .then((auth) => { + if (!auth) { + window.location.reload(); + } else { + console.log("Authenticated"); + } + + createApp(App).mount("#app"); + }) + .catch((error) => { + console.log(error); + console.error("Authenticated Failed"); + }); + +keycloak.onTokenExpired = () => { + console.log("Token expired"); + + keycloak + .updateToken(30 * 60) + .then(() => { + console.log("Token renewed"); + }) + .catch(() => { + keycloak.login() + }) +} + +export default keycloak; +``` + +## Dotnet + + +We can create a webapi project using Dotnet 6 as follows. + +```bash +cd dotnet +dotnet new sln -n Secured +dotnet new webapi -o Secured.API +cd Secured.API +dotnet watch run +``` + +Thanks to the standards supported by Keycloak and Dotnet, we can only authorize with the configurations we will make in Program.cs without writing a special code. In this code, we verify where the token came from with the issuer, and thanks to IssuerSigningKey, we can verify the generated token without going to the keycloak. + +RequireHttpsMetadata should only be used for development enviroment, if you have an application running in production enviroment you can delete this line and its value will evaluate to true. + +```cs +builder.Services.AddSwaggerGen(); + +// ... + +var issuer = builder.Configuration["Jwt:Issuer"]; +var key = builder.Configuration["Jwt:Key"]; +builder.Services.AddAuthentication(options => + { + options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; + options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; + }) + .AddJwtBearer(o => + { + o.Authority = issuer; + o.RequireHttpsMetadata = true; + o.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters + { + ValidateIssuer = true, + ValidateAudience = false, + ValidateLifetime = true, + ValidateIssuerSigningKey = true, + ValidIssuer = issuer, + IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(key)) + }; + }); + +// ... + +var app = builder.Build(); + +// ... + +app.UseAuthentication(); +app.UseAuthorization(); + +// ... + +app.Run(); +``` + +We can access it at builder.Configuration by keeping the data as below in the appsettings.json file. You can access the RSA256 key from the RSA256 Kid value in the Keys tab in the Realm settings menu under the relevant realm in the keycloak admin panel. + +```json +"Jwt": { + "Issuer": "https://localhost:8080/realms/dotnet-vue", + "Key": "secret-rsa-key" +}, + +``` + +## Consuming Dotnet API from Vue app + +First we add CORS settings and Authorize attributes in our dotnet project. For this, we edit `Program.cs` as follows. + +```cs + +// ... + +builder.Services.AddCors(options => +{ + options.AddDefaultPolicy( + policy => policy.WithOrigins("http://localhost:5137") + .AllowAnyHeader() + .AllowAnyMethod() + .AllowAnyOrigin() + ); +}); + +// ... + +var app = builder.Build(); + +// app.UseHttpsRedirection(); + +app.UseCors(); +``` + +The WeatherForecastController.cs file provides security with the following attribute. + +```cs +// ... + +[Authorize] +public class WeatherForecastController : ControllerBase + +// ... +``` + +Then, we install the axios library in our vue application with the following command. + +```bash +npm install axios +``` + +After installation, we create a folder named services and make axios configurations in our file named `base.api.js`. + +```js + +import axios from "axios"; +import keycloak from "../main" + +const http = axios.create({ + baseURL: "http://localhost:5050", + headers: { + "Content-Type": "application/json", + }, +}); + +http.interceptors.request.use( + async (config) => { + const token = localStorage.getItem("vue-token"); + config.headers = { + Authorization: `Bearer ${keycloak.token}`, + Accept: "application/json", + }; + return config; + }, + (error) => { + Promise.reject(error); + } +); + +export default http; +``` + +To use the WeatherForecast endpoint, which is an example endpoint, we create the `weather.api.js` file in the same directory. + +```js + +import axios from "./base.api"; + +export const getWeather = async () => { + const response = await axios.get(`/WeatherForecast`); + return response.data; +} +``` + +We display the data by making our API request in `App.vue`, here we can log out with the logout method. + +```vue + + + + +``` + +If you want to access the source code, you can find the whole project on my GitHub account: + +[GitHub - berkslv/lecture-dotnet-vue-keycloak](https://github.com/berkslv/lecture-dotnet-vue-keycloak) + +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/how-to-translate-400-lines-of-code-comment-to-english-in-5-minutes-using-bash-scripts.md b/content/english/posts/how-to-translate-400-lines-of-code-comment-to-english-in-5-minutes-using-bash-scripts.md deleted file mode 100644 index 206fecb..0000000 --- a/content/english/posts/how-to-translate-400-lines-of-code-comment-to-english-in-5-minutes-using-bash-scripts.md +++ /dev/null @@ -1,119 +0,0 @@ -+++ -title = "How to translate 400 lines of code comment to English in 5 minutes? Using Bash Scripts!" -slug = "how-to-translate-400-lines-of-code-comment-to-english-in-5-minutes-using-bash-scripts" -date = "2022-03-12T11:29:25+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -tags = ["bash", "bash script", "zsh", "automation"] -keywords = ["bash", "bash script", "zsh", "automation"] -description = "We'll cover how to update multiple pieces of data at once using the power of Bash." -showFullContent = false -readingTime = false -+++ - -Perhaps the most enjoyable part of being a computer engineer is automating simple tasks that will take a lot of time but do not require qualifications. In this article, I will briefly explain how I translated the comments in my project, which includes 400 lines of Turkish code comments, into English. - -My [CollegeHub](https://github.com/college-hub) service project will be publicly released on GitHub very soon. I wrote the docs in Turkish, which is my native language, for convenience during the development phase, but in principle, I do not want to use a language other than English in the software source file 😁. - -# Let's jump right into how I do this. - -I use VSC as IDE and I wrote my codes in C#. First, let's explain the steps one by one and take a look at the details: - -1. I copied the comments on lines containing "//" into a txt file. - -2. I copied the contents of the txt file containing line-by-line comments and made a machine translation using Google Translate. - -3. I handled the translation of each file separately, file by file. - -4. I updated the words matching the Bash Script with their English. - - -## Step 1 - -I selected all the comments containing "//" in a file with `Ctrl+D`, copied and pasted them into the "translate.tr.txt" file. I then cleared the Comment marks. I have a file like below. In this way, I gathered the comments of all the files that I will translate in a single file. - -```txt -# translate.tr.txt - -- AuthManager.cs -Kayıt işleminden önce validasyon yapılır. -Parola hashlenerek oluşturulur. -... - -- CommentManager.cs -Verilen id ile Post döndürülür. -Post kontrol edilir. -... - -``` - -```txt -# translate.en.txt - -- AuthManager.cs -Validation is done before registration. -It is created by hashing the password. -... - -- CommentManager.cs -Post is returned with the given id. -The post is checked. -... - -``` - -## Step 2 - -In this file, I selected all of them with `Ctrl+A` and translated them via Google Translate. If this txt file is too long, you can do this translation piece by piece (that's how I did it.). - -## Step 3 - -To run the bash script, I kept the translations and original versions of each script files in two separate files. I have created the following three files in the same location as the source files. - -```txt - -|- translate.AuthManager.en.txt -|- translate.AuthManager.tr.txt -|- replace.bash - -``` - -## Step 4 - -We are at the most exciting part, bash script writing. I am definitely not a Bash Script guru, I only learn the part that I may need thanks to Google, but I plan to improve myself in this subject in the near future because with Bash Script you gain a lot of flexibility on the file system. If we explain the following piece of code simply; - -First, the variables containing the filenames are defined, then the while loop is opened for the file `tr` (The loop repeats the number of rows.), while in the while the matching lines in the file `en` are read (take care that the translations are on the same lines.). Finally, the matching texts are updated using the `sed` command. - -```bash -#!/bin/bash - -# Assign the filenames -filename="AuthManager.cs" -filenameTR="translate.AuthManager.tr.txt" -filenameEN="translate.AuthManager.en.txt" - -# initalize while loop -n=1 -# reading each line -while read line; do -# assign tr file line-by-line to search variable -search=$line -# assign en file line-by-line to replace variable -replace=$(sed "$n q;d" $filenameEN) - -# check if search and replace null -if [[ $search != "" && $replace != "" ]]; then -# replace all matching texts. -sed -i '' "s/$search/$replace/" $filename -fi - -n=$((n+1)) -done < $filenameTR - -``` - ---- - -## Last word - -Thank you very much for your time. If you find this content valuable, you can specify it with a short comment. 🥳 diff --git a/content/english/posts/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway.md b/content/english/posts/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway.md new file mode 100644 index 0000000..0808248 --- /dev/null +++ b/content/english/posts/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway.md @@ -0,0 +1,351 @@ ++++ +title = "How to use Ocelot and Keycloak together to secure Microservices from API Gateway" +date = "2024-01-30T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["keycloak","ocelot","microservices","api gateway","oauth"] +description = "In the dynamic landscape of microservices architecture, the need for robust security has become much more important. As organizations break down their applications into smaller, independently…" +showFullContent = false +readingTime = true ++++ + +In the dynamic landscape of microservices architecture, the need for robust security has become much more important. As organizations break down their applications into smaller, independently deployable services, ensuring the integrity and confidentiality of data exchanged between these services becomes a critical concern. + +In this blog post, we will explore a comprehensive solution for securing microservices using Ocelot, an API Gateway with Keycloak, a powerful open-source identity and access management solution. By placing Keycloak behind the API Gateway, we’ll delve into how this integration safeguards resources, authenticates requests to other services, and authorizes access using claims, offering a seamless and secure communication framework for your microservices ecosystem. Let’s get started. + +## Create Keycloak instance + +Running keycloak within Docker is very easy. Just place this Dockerfile in /Identity directory in your project. You can find all of the codes in my repository from end of the post. + +```Dockerfile +FROM quay.io/keycloak/keycloak:latest as builder + +# Enable health and metrics support +ENV KC_HEALTH_ENABLED=true +ENV KC_METRICS_ENABLED=true + +# Configure a database vendor +ENV KC_DB=postgres + +WORKDIR /opt/keycloak +# for demonstration purposes only, please make sure to use proper certificates in production instead +RUN keytool -genkeypair -storepass password -storetype PKCS12 -keyalg RSA -keysize 2048 -dname "CN=server" -alias server -ext "SAN:c=DNS:localhost,IP:127.0.0.1" -keystore conf/server.keystore +RUN /opt/keycloak/bin/kc.sh build + +FROM quay.io/keycloak/keycloak:latest +COPY --from=builder /opt/keycloak/ /opt/keycloak/ + +# change these values to point to a running postgres instance +ENTRYPOINT ["/opt/keycloak/bin/kc.sh"] +``` + +And create docker-compose.yml file that access /Identity/Dockerfile and builds image from it. We can use base image rather than custom Dockerfile but with this approach you can customize much more features of Keycloak like frontend theme and secret keys. + +```yaml +version: "3" + +services: + # PostgreSQL for keycloak + secured-identity-db: + container_name: secured-identity-db + image: postgres:16-alpine + ports: + - 6063:5432 + expose: + - 6063 + volumes: + - ./data/secured-identity-db:/var/lib/postgresql/data + restart: always + environment: + - POSTGRES_PASSWORD=myStrongPassword123 + - POSTGRES_USER=keycloak + - POSTGRES_DB=keycloak + networks: + - secured-network + + # Keycloak + secured-identity: + container_name: secured-identity + build: ./Keycloak + command: ["start-dev"] + ports: + - 5053:8080 + expose: + - 5053 + environment: + - KEYCLOAK_ADMIN=admin + - KEYCLOAK_ADMIN_PASSWORD=admin + - KC_HOSTNAME_URL=http://localhost:5050/identity + - KC_DB=postgres + - KC_DB_USERNAME=keycloak + - KC_DB_PASSWORD=myStrongPassword123 + - KC_DB_URL=jdbc:postgresql://secured-identity-db:5432/keycloak + depends_on: + - secured-identity-db + networks: + - secured-network + +networks: + secured-network: + driver: bridge + +volumes: + secured-data: + driver: local +``` + +And we just need to run docker from this docker-compose.yml file. + +```bash +docker compose build +docker compose up -d +``` + +## Put keycloak behind Ocelot API Gateway to protect resources + +Microservice architecture with Ocelot and Keycloak +

Microservice architecture with Ocelot and Keycloak

+ + +It is important to secure your identity service (keycloak) and reduce the attack vector by limiting the endpoints that can receive requests like other services. Keycloak admin panel should not be accessed from an external network, only people on this network can access this feature. For further readings consider visit Keycloak documentation about [Using a reverse proxy](https://www.keycloak.org/server/reverseproxy). + +Exposed path recommendations +

Exposed path recommendations

+ +First, let’s create Ocelot project. I use Dotnet 8 + +```bash +dotnet new sln -n Secured +dotnet new webapi -o Secured.ApiGateway +cd Secured.ApiGateway/ +dotnet add package Ocelot --version 22.0.1 +``` + +After we successfully created the project, modify Program.cs as follows and create ocelot.json file in root directory. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + +builder.Services.AddOcelot(builder.Configuration); + +var app = builder.Build(); + +await app.UseOcelot(); +await app.RunAsync(); +``` + +After that, we can use this ocelot.json file to limit access to our keycloak application running on localhost:5053. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/realms/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5053 + } + ], + "UpstreamPathTemplate": "/identity/realms/{everything}", + "UpstreamHttpMethod": [ "Get", "Post", "Put", "Delete" ] + }, + { + "DownstreamPathTemplate": "/resources/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5053 + } + ], + "UpstreamPathTemplate": "/identity/resources/{everything}", + "UpstreamHttpMethod": [ "Get" ] + }, + { + "DownstreamPathTemplate": "/js/{everything}", + "DownstreamScheme": "http", + "DownstreamHostAndPorts": [ + { + "Host": "localhost", + "Port": 5053 + } + ], + "UpstreamPathTemplate": "/identity/js/{everything}", + "UpstreamHttpMethod": [ "Get" ] + } + ], + "GlobalConfiguration": { + "BaseUrl": "https://localhost:5050" + } +} +``` + +After this configurations you can access keycloak from localhost:5050 that is the API Gateway’s address. After these configurations, you can access keycloak from localhost:5050, which is the address of the API Gateway, but you will not be able to access the Keycloak admin panel through this address because we have limited access. + +For making our Keycloak configurations, we can log in to the admin panel at the keycloak address that is still accessible locally, that is, localhost:5053. Note that in the deployment scenario, we will ensure that only Ocelot has access to the external internet. + +In localhost:5053 we can enter **admin** for username and **admin** for password. We can change this credentials from docker-compose.yml file. + +Keycloak + +After logged in we create new realm for managing users and client for our application from this dropdown menu. After clicking the Create realm button, simply enter the **secured** as the realm name and leave everything as is. + +Keycloak + +In clients tab click Create client. name Client ID as **postman** and valid redirect uri as [**https://oauth.pstmn.io/v1/callback**](https://oauth.pstmn.io/v1/callback). leave other things as is. + +Keycloak + +In the users tab, add users to log in to Keycloak. + +Keycloak + +After these steps we can login to our keycloak with newly created user to get access token. For accessing API gateway I use Postman. In postman you can logged in with Keycloak. In Authorization section select OAuth 2.0 and configure as follows and click Get New Access Token. Postman will open browser window and redirect you to Keycloak login page, enter credentials for your new user. Don’t enter admin credentials, this credentials only valid in master realm. + +```txt +Grant type: Authorization code +Auth URL: http://localhost:5050/identity/realms/secured/protocol/openid-connect/auth +Access Token URL: http://localhost:5050/identity/realms/secured/protocol/openid-connect/token +Client ID: postman +Scope: openid profile roles +``` + +If you did everything right, you have successfully authenticated using Postaman with Keycloak behind the API Gateway 🎉 + +## Authenticate requests to other services + +If you want to authenticate a request in Ocelot, make the following updates to Program.cs and ocelot.json. MetadataAddress gets the keys required for JWT validation. However, if you want to avoid extra network calls, you can place the keys in the appsettings.json file and use them. + +Authentication completed with AuthenticationOptions in ocelot.json file, here we say that we will send our JWT token along with the Bearer prefix and JWT token validation done with configuration from Program.cs + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services + .AddAuthentication(JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, o => + { + o.MetadataAddress = "http://localhost:5050/identity/realms/secured/.well-known/openid-configuration"; + o.RequireHttpsMetadata = false; + o.Authority = "http://localhost:5050/realms/secured"; + o.Audience = "account"; + }); + +builder.Configuration.AddJsonFile("ocelot.json", optional: false, reloadOnChange: true); + + +builder.Services.AddOcelot(builder.Configuration); + +var app = builder.Build(); + +app.UseAuthentication(); +app.UseAuthorization(); + +await app.UseOcelot(); +await app.RunAsync(); +``` + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/get", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "httpbin.org", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/test", + "UpstreamHttpMethod": [ "Get" ], + "AuthenticationOptions": { + "AuthenticationProviderKey": "Bearer" + } + }, + +//... +``` + +## Authorize requests to other services + +Authorization is slightly more difficult and requires additional work because Keycloak place user roles in nested form in JWT payload. It is something like following. As you can see roles field is placed under realm_access field. This creates confusion in Ocelot, because Ocelot reads claims as string or object values and does not evaluate nested fields. To avoid this problem we configure Keycloak token mapper and don’t use nested form. + +```json +{ + //... + "exp": 1706600524, + "realm_access": { + "roles": [ + "offline_access", + "default-roles-microcommerce", + "uma_authorization", + "customer" + ] + }, + "resource_access": { + "account": { + "roles": [ + "manage-account", + "manage-account-links", + "view-profile" + ] + } + }, + "scope": "openid email profile", + "preferred_username": "berkslv", +} +``` + +For this custom mapping, click Client scopes from left menu and select roles. + +Keycloak + +Under the roles, select Mappers section and then click realm roles. + +Keycloak + +In the menu that opens, realm_access.roles is entered in the Token Claim Name option. We update this to **realm_roles**. + +Keycloak + +After making these configurations, we update our token by logging in again via Postman. We make the following update in the ocelot.json file so that the requests we make with the updated token are subject to claim control by Ocelot. If we don’t have **customer** role in our user we get 403 response. You can create and assing your custom roles in Keycloak admin panel. + +```json +{ + "Routes": [ + { + "DownstreamPathTemplate": "/get", + "DownstreamScheme": "https", + "DownstreamHostAndPorts": [ + { + "Host": "httpbin.org", + "Port": 443 + } + ], + "UpstreamPathTemplate": "/test", + "UpstreamHttpMethod": [ "Get" ], + "AuthenticationOptions": { + "AuthenticationProviderKey": "Bearer" + }, + "RouteClaimsRequirement": { + "realm_roles": "customer" + } + }, + +//... +``` + +If you want to access the source code, you can find the whole project on my GitHub account: + +[GitHub - berkslv/lecture-ocelot-and-keycloak](https://github.com/berkslv/lecture-ocelot-and-keycloak) + +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net.md b/content/english/posts/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net.md new file mode 100644 index 0000000..9049839 --- /dev/null +++ b/content/english/posts/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net.md @@ -0,0 +1,350 @@ ++++ +title = "How to Use RSA for Encryption in JavaScript and Decryption in .NET" +date = "2024-03-07T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["rsa", "cryptography", ".NET", "security"] +description = "In the digital world, securing communication between different systems is paramount. RSA encryption is an asymmetric encryption algorithm and provides a robust way to achieve this by using a pair of…" +showFullContent = false +readingTime = true ++++ + +In the digital world, securing communication between different systems is paramount. RSA encryption is an asymmetric encryption algorithm and provides a robust way to achieve this by using a pair of keys: one for encryption and one for decryption. On the other hand, symmetric encryption algorithms, such as AES, use a single key for encryption and decryption. For these reasons, asymmetric encryption algorithms may be preferred in order to use the key publicly and avoid problems even if it falls into the hands of an attacker. + +In this blog post I will guide you through implementing RSA key generation with OpenSSL, encryption in JavaScript and decryption in .NET, ensuring secure data exchange between a frontend application and a backend service. + +## Understanding RSA Encryption + + +RSA (Rivest-Shamir-Adleman) is one of the first public-key cryptosystems and is widely used for secure data transmission. It involves two keys: a public key, which can be shared openly, for encrypting messages, and a private key, must be kept secret, for decrypting them. The strength of RSA lies in the difficulty of factoring the product of two large prime numbers, which underpins its security. + +## Generation of Public/Private key pairs with OpenSSL + + +One of the foundational steps in implementing RSA encryption and decryption is the generation of public and private key pairs. OpenSSL, a robust open-source tool for cryptography, offers a straightforward way to generate these keys. However, this step is optional. .NET libraries successfully generate RSA keys. However, using open source tools like this allows us to be ensure of the standards. This section will guide you through the process of generating an RSA public/private key pair using OpenSSL. + +We can start with generation of private key as follows: + +```bash +$ openssl genrsa -out privatekey.pem 2048 + +$ cat privatekey.pem +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDuOitcIPd5UpGN +/4GEwNhc4fAmGP6fREJJk/byRgiLiWcyXwdoPwXdXjKSjkSIcETdz/hLAlb6+zNY +Gi4Ap0S5flZSRiKh60xWRA4vZVgsLqhEi2IsclSlWu3R7KD5dkBGsyf5xlCfNSs4 +DbYYMgrXXZxXQ0c4qCikpnEB6OpqSzm19Cwrdq9GwophoAPxnf00d7S+y5QpRa+l +Ca70xQ42FxBpMuK76a1g6i+JtmPgTYqXZ0FCMnUEzBq+u3N8LKoNzcSNdrLgvmHQ +oOLFA/9BPe34YgLxoCUi+HhEiLSnvgz/Kn1yprvsz37baYizU/YWFG8s80SPmFsY +JARxvZiJAgMBAAECggEAEboc42Jln+7Lu34NmIAUKZc7fE5EVjwpVZnP2Lfq510+ +Y2JsZe64pEAf8cVp5qA6E6pn3scKC2uZZr8t+Kj5xXbX/A/RgGyGte2jA5ZeQQ1o +wo7/q3CHiXVyeHpMATwyLMCkoyLFeY9mi6mDiRniMiP6YAj22gmzWoszIhH/rZCO +NjdpWRsIkFp8jIiyRhhoaBoJtZ1qV3twk8HH590rgjS7QUGJWK9uUsi5SDh69MsR +r/SJ00gTMYFozFd6dB29aKj/p8fSk6qzh2noxU37AuiVc1A2H8Z6SAfRUDSvPkxA +FdqVr62iLWmypBX2VYQ2p1pH1N0v8oG7NTxtrxgRTQKBgQD3YdxKNT0dWZJRGPFC +DIRZVn86UgDAHRw29aToLARnepE5XOrLSmlGMqbK8vzRyUnlctJyG7x5ure2BMp4 +gKSLuhz48n7ZvpHyCVnNsqqkc21iM/whL/yKiI11Jz5koXmRDltz9IioQH/PwXvD +RJbjxISHjG5YEKlVmztWtTup/QKBgQD2hqouE8JLePAUzK3SlfgVTlmaHDUeJev4 +u95RWPnMNOOwLUWbGYiKBoJ8uc2nSHmxIvEvEPGNwafR6etf0YocpwDfF5pnPDjk +yA4hOdZrCyYa/UDqkB5pHTOZj5iVct1hBNbiANz6kL7Pl+H221U+HxD9rhaDPDez +gdYPhLF4fQKBgQCdi2QomlkufOHS5eiXoLMU1iI2eQzjTGawlaYM+iNf503fU05w +YxZCT3WroC3kSKXYC1T8uK9CcugWclyje4HPPpq+7GhesZ/unYfkmRlVm/EYbnTu +icnyS96Ssohou/FYsOULJrt1M4ZyQA1aoS7bJUclsAiB6zJ8Q1z57ndt3QKBgEwa +9p6S6wBxrWw+Y8sHETdCoNa6rotIGbkIBnIGjddE3KWe1EY1c7lomQ/4LEzgSvEs +YFivWmLwzeY32LoT7hc6V0KH/tqv9MBsIjCPsmoJXxIl7Mx9AWZh5XQaqHg6pa01 +2UCcE5wm40EjGlcjDwXGgXBPNhP9mxSHmJXh2QfhAoGANouUrF3qv8ofb1HGrUXx +DO/WkGMzwbkNmwwdgcGYVsGr3Dv/zx1y3TJEvqNh0ViVgmfdYveCo3VrSawq890U +GJZXld2KFE1icEJpNoURkM9okZMzTDIMs+r4vq5Or2jB04mMuoCbG6rjfqUMtkll +oAIFOSKH+F3TrXjNgy4/juo= +-----END PRIVATE KEY----- +``` + +After we have successfully created the private key, we can create the public key with the private key we created. + +```bash +$ openssl rsa -in privatekey.pem -outform PEM -pubout -out publickey.pem + +$ cat privatekey.pem +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7jorXCD3eVKRjf+BhMDY +XOHwJhj+n0RCSZP28kYIi4lnMl8HaD8F3V4yko5EiHBE3c/4SwJW+vszWBouAKdE +uX5WUkYioetMVkQOL2VYLC6oRItiLHJUpVrt0eyg+XZARrMn+cZQnzUrOA22GDIK +112cV0NHOKgopKZxAejqaks5tfQsK3avRsKKYaAD8Z39NHe0vsuUKUWvpQmu9MUO +NhcQaTLiu+mtYOovibZj4E2Kl2dBQjJ1BMwavrtzfCyqDc3EjXay4L5h0KDixQP/ +QT3t+GIC8aAlIvh4RIi0p74M/yp9cqa77M9+22mIs1P2FhRvLPNEj5hbGCQEcb2Y +iQIDAQAB +-----END PUBLIC KEY----- +``` + +## Setting Up the Environment for .NET RSA Decryption + + +For .NET decryption I will use .NET 8 and use the System.Security.Cryptography namespace. We don’t need any other nuget package for RSA decryption. For RSA Decryption I created a Helper class like this. + +We read RsaKey from appsettings.json. You should add \n for newlines in RsaKey field. We put this key in appsettings.json because we want to use same keys in every request. But If you leave it to the responsibility of the .NET library, in every startup there will be newly generated key. + +```json +// appsettings.json +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*", + "RsaKey": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCuan+y++Eqc2Vx3QIt9gv0r6rzfcxpsBRubcWCmI+8tqb40oflv5dViPSiCSWNgg5xKk7K8WTeEQQ6NDg1IJ2OwoQ2dfzat5qlpfV9EeF3u8iTY/hyQaaYFwB77cV9t5Czb8oG/+IIOByorJVds9tAoKjssKUZ3W9IU7ffElZjZrbjoiy/H4z8u9fOq8IL9Zf3pHgzv2FxF4BPJamqr4s1VtMqGJ5g18wV1OD9gcz9pJOHHUVieZ0+xP4WD4+1wCv/uwEgIcqmEs0os6birHZL1X/CSqBSPc8e/+kkZyyzoF+MBWPzvAwmW32alIxz2ZV0Z+jJtoOrh/qVqbrGAf+RAgMBAAECggEAEWYu4IJHg0ZZOZtgRwj7RtjKXzluraFi3GRHdoB1IFCBRiOsamMrO91qeAqdDCmL+saLbyvXEd8VMqA4djZPeWkWqt8ozwHPY9RzMZuZyCm7t9Zad71sWtI6mmJNF/46qWfOudWHbSX51+rFiMAzMFaGm3wAsFyaaBbv6gkohIhVrrMvpIuV8X8JNI8/VNlOR6vExHd/3uWKi8vPrFEFTNQ1UE+WJDkVojcmx0t63jlP8C/O4ofaJeCYRkAaM9+FXnM+jVNf0qZU+3JGYmy1R/B7L+LyrbG2uYTaBa1Ba9zGsXD1HQ4dctK+MltBd4p+MWgHAQV+xuVmGGDkylpb8QKBgQDP9zY4UzLmnt0werJF9SGmAJU51Y/+gannUC/+XYqnK/NA5iUDR86rlYCJbqQCceyMYJSYuePumygWOVL9rHnu1/fTMG/sXnRktmTSFemgeCYfPyn8/KeZKn6E5xw8GbtfLJLA8uM4DakDf9vSpilKDYT4UbZyKgq7/yodPXbgCQKBgQDWs4gL4yOtDLAY/OQKZvoyXClapldkVFRzf97pBJQGLYfdTzkVFviWsgI/yrA+t5tlzaWtZieuv2KsSmazVyO2W5ei9HILx4yRFK7K56OM5o7wqjN62ZmF0R+422YNaAyqYAd9nhLjgdsrmZ9FTxhQ91asBSH1AO3j6xaZUvh1SQKBgAZuo/ur/xgI89hrAxaM1WSYAgWO6Gw7wHCKF2HrrL0s69InDCAE2YyPDDGz/ViiA2n4FsB+h2E65UuCrGFyMzdC8MRUbDHIXhs7VPT2fopbDPrMblUHz3s6SD1+FG57cUMpUsSq/oIeUgrsqnTidMZ4kpNHm7f+OuTDqJ7M5t9ZAoGAAfm2570YR/BU8nXpNztJVAtLCh17sl2gRUvI5kX3grMKi/u9n7cNZH2Qzbt0sa8Iy///ZUAKX249Xy50EXRczMG8/G/ZWMhmP7N8BDvrYlGAwTAftyKnafbJnu7N2pO5ghvOFdbNf7BjLtyD/aRDqgMMlhqZ/GIczjsMgy6jQJkCgYATxkjdW/mnWLDDZAYm1V10Gnm7YDMwX3gKdhpoYleeeapZiDlurkR3YAEvQezcnhv3NpjaKQtF1/3Q0fZJ0wBc5FflaevHRjHTa0IrU5QIa48dLDx+jxn+cUCFx9kirkb7JcfJLZWtToPMLfCZt324P2ogZgWBwweVPiz1/voEXw==\n-----END PRIVATE KEY-----" +} +``` + +```cs +// Helpers/RsaEncryptionHelper.cs +public class RsaEncryptionHelper +{ + private readonly RSA _rsa = RSA.Create(); + + public RsaEncryptionHelper(IConfiguration configuration) + { + var privateKeyPem = configuration["RsaKey"]; + if (string.IsNullOrEmpty(privateKeyPem)) throw new ArgumentNullException(nameof(privateKeyPem)); + ImportKey(privateKeyPem); + } + + + public void ImportKey(string privateKey) + { + _rsa.ImportFromPem(privateKey.ToCharArray()); + } + + public string GetPublicKey() + { + var publicKey = _rsa.ExportSubjectPublicKeyInfoPem(); + return publicKey; + } + + public string GetPrivateKey() + { + var privateKey = _rsa.ExportPkcs8PrivateKeyPem(); + return privateKey; + } + + public string Encrypt(string data) + { + var dataBytes = Encoding.UTF8.GetBytes(data); + var encryptedData = _rsa.Encrypt(dataBytes, RSAEncryptionPadding.OaepSHA256); + return Convert.ToBase64String(encryptedData); + } + + public string Decrypt(string data) + { + var dataBytes = Convert.FromBase64String(data); + var decryptedData = _rsa.Decrypt(dataBytes, RSAEncryptionPadding.OaepSHA256); + return Encoding.UTF8.GetString(decryptedData); + } +} +``` + +And use this helper class inside of EncryptionController to use helper class’s methods. In production enviroments You should not be expose PrivateKey to anyone. This should be kept secret. + +```cs +// Controllers/EncryptionController.cs +[ApiController] +[Route("api/[controller]")] +public class EncryptionController : ControllerBase +{ + private readonly RsaEncryptionHelper _rsaEncryptionHelper; + + public EncryptionController(RsaEncryptionHelper rsaEncryptionHelper) + { + _rsaEncryptionHelper = rsaEncryptionHelper; + } + + [HttpPost("Encrypt")] + public string Encrypt([FromBody] EncryptionRequest request) + { + return _rsaEncryptionHelper.Encrypt(request.Data); + } + + [HttpPost("Decrypt")] + public string Decrypt([FromBody] EncryptionRequest request) + { + return _rsaEncryptionHelper.Decrypt(request.Data); + } + + [HttpGet("PublicKey")] + public string PublicKey() + { + return _rsaEncryptionHelper.GetPublicKey(); + } + + [HttpGet("PrivateKey")] + public string PrivateKey() + { + return _rsaEncryptionHelper.GetPrivateKey(); + } + + [HttpPost("ImportKey")] + public void ImportKey([FromBody] ImportKeyRequest request) + { + _rsaEncryptionHelper.ImportKey(request.PrivateKey); + } + +} + +public record ImportKeyRequest +{ + public string PrivateKey { get; init; } = null!; +} + +public record EncryptionRequest +{ + public string Data { get; init; } = null!; +} +``` + +After this development we made in the backend side we can continue to JavaScript side. Thanks to all we’ve done we share our public key from endpoint with client, then clients could use this public key to encrypt their data for secure transmission between sides. + +## Setting Up the Development environment for JavaScript RSA Encryption + + +Before diving into the code, ensure your environment is set up for JavaScript development. For this example, we’ll use the Web Crypto API, which is built into modern browsers and provides cryptographic operations including RSA encryption. Therefore you don’t need to download any npm packages. + +If I am going to develop a browser application with JavaScript, I usually start with Vite. To do this, we can start a project using vanilla js or React by entering the following command. I will develop a small React project for demonstration purposes, but it will be enough even if you stick with vanilla js. + +```bash +npm create vite@latest +``` + +After that I will create a src/utils/RsaEncryptionHelper.js file and fill that file as follows. With this file’s methods you can encrypt and decrypt with RSA algorithm but usually decryption is a resposibility of backend side because decryption requires private key and javascript client’s should not be store or access private keys publicly because of security reasons. But I am sharing this in case anyone wants to try something similar to two-way key encryption like in the SSL. + +```js +// src/utils/RsaEncryptionHelper.js +const encryptAlgorithm = { + name: "RSA-OAEP", + modulusLength: 2048, + publicExponent: new Uint8Array([1, 0, 1]), + extractable: true, + hash: { + name: "SHA-256", + }, +}; + +function arrayBufferToBase64(arrayBuffer) { + const byteArray = new Uint8Array(arrayBuffer); + let byteString = ""; + for (let i = 0; i < byteArray.byteLength; i++) { + byteString += String.fromCharCode(byteArray[i]); + } + const b64 = window.btoa(byteString); + return b64; +} + +function base64StringToArrayBuffer(b64str) { + const byteStr = atob(b64str); + const bytes = new Uint8Array(byteStr.length); + for (let i = 0; i < byteStr.length; i++) { + bytes[i] = byteStr.charCodeAt(i); + } + return bytes.buffer; +} + +function convertPemToArrayBuffer(pem) { + const lines = pem.split("\n"); + let encoded = ""; + for (let i = 0; i < lines.length; i++) { + if ( + lines[i].trim().length > 0 && + lines[i].indexOf("-----BEGIN RSA PRIVATE KEY-----") < 0 && + lines[i].indexOf("-----BEGIN PRIVATE KEY-----") < 0 && + lines[i].indexOf("-----BEGIN PUBLIC KEY-----") < 0 && + lines[i].indexOf("-----END RSA PRIVATE KEY-----") < 0 && + lines[i].indexOf("-----END PRIVATE KEY-----") < 0 && + lines[i].indexOf("-----END PUBLIC KEY-----") < 0 + ) { + encoded += lines[i].trim(); + } + } + return base64StringToArrayBuffer(encoded); +} + +export const encryptRsa = async (str, pemString) => { + try { + // convert string into ArrayBuffer + const encodedPlaintext = new TextEncoder().encode(str).buffer; + const keyArrayBuffer = convertPemToArrayBuffer(pemString); + // import public key + const secretKey = await crypto.subtle.importKey( + "spki", + keyArrayBuffer, + encryptAlgorithm, + true, + ["encrypt"] + ); + // encrypt the text with the public key + const encrypted = await crypto.subtle.encrypt( + { + name: "RSA-OAEP", + }, + secretKey, + encodedPlaintext + ); + // store data into base64 string + return arrayBufferToBase64(encrypted); + } catch (error) { + console.error("Encryption Error:", error); + } +}; + +export const decryptRsa = async (str, pemString) => { + try { + // convert base64 encoded input string into ArrayBuffer + const encodedPlaintext = base64StringToArrayBuffer(str); + const keyArrayBuffer = convertPemToArrayBuffer(pemString); + // import private key + const secretKey = await crypto.subtle.importKey( + "pkcs8", + keyArrayBuffer, + encryptAlgorithm, + true, + ["decrypt"] + ); + // decrypt the text with the public key + const decrypted = await crypto.subtle.decrypt( + { + name: "RSA-OAEP", + }, + secretKey, + encodedPlaintext + ); + // decode the decrypted ArrayBuffer output + const uint8Array = new Uint8Array(decrypted); + const textDecoder = new TextDecoder(); + const decodedString = textDecoder.decode(uint8Array); + return decodedString; + } catch (error) { + console.error("Decryption Error:", error); + } +}; +``` + +I developed a small React project using this helper methods. You can use your public and private keys that will be shared from .NET API to encrypt and decrypt data, but keep in mind decryption should be doing in backend side with private key that kept secret. + + + + +RSA Encryption (lecture-rsa-dotnet-javascript.vercel.app) +

RSA Encryption (lecture-rsa-dotnet-javascript.vercel.app)

+ +
+ + +If you want to access the source code, you can find the whole project on my GitHub account: + +[GitHub - berkslv/lecture-rsa-dotnet-javascript](https://github.com/berkslv/lecture-rsa-dotnet-javascript) + + +--- + +## Conclusion + +Thank you for reading 🎉 Don't miss out on the latest updates and insights in the world of software development. Follow me on [@berkslv](https://x.com/berkslv) to stay connected and join the conversation \ No newline at end of file diff --git a/content/english/posts/privacy-policy-for-space-invader-zero-point.md b/content/english/posts/privacy-policy-for-space-invader-zero-point.md deleted file mode 100644 index 59dc2fc..0000000 --- a/content/english/posts/privacy-policy-for-space-invader-zero-point.md +++ /dev/null @@ -1,212 +0,0 @@ -+++ -title = "Privact policy for Space Invader: Zero Point" -slug = "privacy-policy-for-space-invader-zero-point" -date = "2023-05-12T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -tags = ["privacy"] -keywords = ["privacy"] -description = "Privacy policy for Space Invader Game" -showFullContent = false -readingTime = false -+++ - -# Privacy Policy - -Last updated: May 16, 2023 - -This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You. - -We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the [TermsFeed Privacy Policy Generator](https://www.termsfeed.com/privacy-policy-generator/). - -# Interpretation and Definitions - -## Interpretation - -The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural. - -## Definitions - -For the purposes of this Privacy Policy: - -- __Account__ means a unique account created for You to access our Service or parts of our Service. -- __Affiliate__ means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority. - -- __Application__ refers to Space Invader: Zero Point, the software program provided by the Company. - - - -- __Company__ (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to Space Invader: Zero Point. - - - -- __Country__ refers to: Turkey - -- __Device__ means any device that can access the Service such as a computer, a cellphone or a digital tablet. - - - -- __Personal Data__ is any information that relates to an identified or identifiable individual. - - - -- __Service__ refers to the Application. - -- __Service Provider__ means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used. - - -- __Usage Data__ refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit). - -- __You__ means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable. - - - -# Collecting and Using Your Personal Data - -## Types of Data Collected - -### Personal Data - -While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to: - -- Email address -- First name and last name - - - - -- Usage Data - - - -### Usage Data - -Usage Data is collected automatically when using the Service. - -Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data. - -When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data. - -We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device. - - - - - - - - - -## Use of Your Personal Data - -The Company may use Personal Data for the following purposes: - -- __To provide and maintain our Service__, including to monitor the usage of our Service. -- __To manage Your Account:__ to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user. -- __For the performance of a contract:__ the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service. -- __To contact You:__ To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation. -- __To provide You__ with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information. -- __To manage Your requests:__ To attend and manage Your requests to Us. - -- __For business transfers:__ We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred. -- __For other purposes__: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience. - -We may share Your personal information in the following situations: - -- __With Service Providers:__ We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You. -- __For business transfers:__ We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company. -- __With Affiliates:__ We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us. -- __With business partners:__ We may share Your information with Our business partners to offer You certain products, services or promotions. -- __With other users:__ when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside. -- __With Your consent__: We may disclose Your personal information for any other purpose with Your consent. - -## Retention of Your Personal Data - -The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies. - -The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods. - -## Transfer of Your Personal Data - -Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction. - -Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer. - -The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information. - -## Delete Your Personal Data - -You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You. - -Our Service may give You the ability to delete certain information about You from within the Service. - -You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us. - -Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so. - -## Disclosure of Your Personal Data - -### Business Transactions - -If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy. - -### Law enforcement - -Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency). - -### Other legal requirements - -The Company may disclose Your Personal Data in the good faith belief that such action is necessary to: - -- Comply with a legal obligation -- Protect and defend the rights or property of the Company -- Prevent or investigate possible wrongdoing in connection with the Service -- Protect the personal safety of Users of the Service or the public -- Protect against legal liability - -## Security of Your Personal Data - -The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security. - - - - - - - - - - - - - - - - -# Children's Privacy - -Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers. - -If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information. - - -# Links to Other Websites - -Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit. - -We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services. - -# Changes to this Privacy Policy - -We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page. - -We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy. - -You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page. - -# Contact Us - -If you have any questions about this Privacy Policy, You can contact us: - - -- By email: berkslv@gmail.com \ No newline at end of file diff --git a/content/english/posts/the-future-of-backend-development-predictions-and-trends.md b/content/english/posts/the-future-of-backend-development-predictions-and-trends.md deleted file mode 100644 index 59b1f35..0000000 --- a/content/english/posts/the-future-of-backend-development-predictions-and-trends.md +++ /dev/null @@ -1,41 +0,0 @@ -+++ -title = "The Future of Backend Development: Predictions and Trends" -date = "2023-01-10T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -tags = ["backend", "container", "cloud", "ai development"] -keywords = ["backend", "container", "cloud", "ai development"] -description = "Take a closer look at what the future of backend development might look like, and consider some of the trends and predictions that experts are making." -showFullContent = false -readingTime = true -+++ - -# The Future of Backend Development: Predictions and Trends - - -As a backend developer, you are always looking for ways to improve your skills and stay ahead of the curve. One way to do this is to keep an eye on the future of backend development and stay up-to-date with the latest trends and predictions. In this blog post, we'll take a closer look at what the future of backend development might look like, and consider some of the trends and predictions that experts are making. - - -## Cloud - -One of the biggest trends in backend development is the increasing use of cloud computing. Cloud computing allows developers to build and run applications and services on remote servers, rather than on local servers or personal computers. This has a number of benefits, including scalability, flexibility, and cost-effectiveness. As a result, many experts believe that cloud computing will continue to play a major role in backend development in the future. - - -## Containers - -Another trend in backend development is the growing use of containerization technologies, such as Docker. Containerization allows developers to package their applications and dependencies into lightweight containers that can be easily deployed and run on any platform. This makes it easier to build and deploy applications, and makes it easier to scale them as well. - - -## Microservices - -A third trend in backend development is the increasing use of microservices architecture. In a microservices architecture, an application is broken down into small, independent services that can be developed, deployed, and scaled independently of one another. This makes it easier to build and maintain complex applications, and allows developers to make changes and updates more quickly and easily. - - -## AI powered development - -In addition to these trends, there are also a number of predictions about the future of backend development. One prediction is that machine learning and artificial intelligence (AI) will play a larger role in backend development in the future. For example, machine learning algorithms could be used to optimize the performance of backend systems, or to automate certain tasks that are currently performed manually. Another prediction is that the use of low-code and no-code platforms will increase, making it easier for developers to build and deploy backend systems without having to write a lot of code. - - -## Conclusion - -Overall, it is clear that the future of backend development is bright, with many exciting trends and predictions on the horizon. Whether you are just starting out as a backend developer or are a seasoned pro, it is important to stay up-to-date with these trends and predictions, and to continue learning and growing your skills. By doing so, you can ensure that you are well-prepared for the future of backend development and that you are able to take advantage of the many opportunities that are likely to come your way. diff --git a/content/turkish/_index.html b/content/turkish/_index.html index e24cffc..d6afb83 100644 --- a/content/turkish/_index.html +++ b/content/turkish/_index.html @@ -1,59 +1,57 @@ +++ -title = "Berk Selvi - Software developer" -date = "2023-01-21T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -description = "Hello! My name is Berk Selvi and I am a full-stack web developer specializing in backend and interested in data science. I am currently studying Computer Science at Namık Kemal University and I am also developing side projects that solves real-world problems, and I love doing that!" -layout = "index" -framed = true +++ -
-
-

Hello world!

-
+
+
+
+

Selam, ben Berk Selvi

+

.NET Developer, blogger and builder.

+
+ +
+
+ Berk Selvi +
-
    -
  • - Hello! My name is Berk Selvi and I am a full-stack web developer specializing in backend and interested in data science. I am currently studying Computer Science at Namık Kemal University and I am also developing side projects that solves real-world problems, and I love doing that! -
  • -
  • - Over the past three years, I've built websites for my clients as a freelance developer. When I started university, I trained myself with theoretical knowledge from various sources such as MIT and Stanford. After believing that I was at a sufficient level, I started to develop complex projects like full-stack social media app and C multithread web server -
  • -
  • - Having a vision from long and varied work experience, I have acquired skills such as responsibility, team player, time management and problem solving. I am confident that my vision and soft skills will help me in projects that I will develop alone or as a team member. -
  • -

    -

      -
    • - Front-End: JavaScript, React, Redux, React-Native, HTML5, CSS3 -
    • -
    • - Back-End: .Net Core, Python, Node.js -
    • -
    • - Databases: T-SQL, SQL Server, MySQL, MongoDB -
    • -
    • - Tools: Docker, AWS, Jira, Git, GitHub -
    • -
    -

    -
- - - \ No newline at end of file +
+

Ben kimim?

+

+ Berk Selvi, full stack web geliştirme alanında backend için .Net ve Java ve frontend için React ve Vue ile çalışan bir yazılım geliştiricisidir. Azure sertifikasına sahiptir ve blog yazıları aracılığıyla topluluğa katkıda bulunur. Yeni teknolojiler öğrenmeye isteklidir, takım çalışmasının bir parçası olarak ürün geliştirmekten mutluluk duyar ve DevOps ile ilgilenir. Şu anda Doğuş Teknoloji'nin sigorta ekibinin bir parçasıdır. +

+
diff --git a/content/turkish/posts/5-core-philosophies-that-every-developer-should-have.md b/content/turkish/posts/5-core-philosophies-that-every-developer-should-have.md new file mode 100644 index 0000000..55bd3e8 --- /dev/null +++ b/content/turkish/posts/5-core-philosophies-that-every-developer-should-have.md @@ -0,0 +1,94 @@ ++++ +title = "Her Geliştiricinin Sahip Olması Gereken 5 Temel Felsefe" +date = "2022-03-09T11:01:10+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["öğrenme", "çalışma", "bilgi"] +description = "Öğrenmeyi öğrendiğinizde, neredeyse tüm dünyadaki olanakları bilgiyle açarsınız." +showFullContent = false +readingTime = true ++++ + +Ben kendim, yazılımın bir işten önce bir yaşam felsefesi olduğunu düşünüyorum ve çoğu sorunu bir yazılım geliştirici olarak yaklaşarak ve bu şekilde çözerek büyük keyif alıyorum. Bu makalede, bu felsefenin temel taşlarından bahsedeceğim. + +## İçindekiler + +- Sorumlu bir kişi olun +- Kodunuzda hiçbir şeyi mahvetmeyin +- Mükemmeliyetçi olmayın +- Varlığınız bilginizdir +- Topluluğunuzu geliştirin! + + +## Sorumlu bir kişi olun + +Bir programcı olarak, yazdığınız JavaScript kodunda bir hata varsa, bu hatanın nedeninin C++ derleyicisi olduğunu söyleyemezsiniz. Bu saçma gelebilir, ancak bu durumu kendinize uyarlamayı düşünün! + +Bir iş almadan önce, o işin gereksinimlerini iyi planlayın çünkü projede uzun bir yol kat ettiğinizde ve bir müşteriye veya patronunuza gittiğinizde size geçerli bir soru sorarlar, "neden bunu önceden düşünmediniz" Bu tür durumları göz önünde bulundurarak işe koyulursanız, karşılaşabileceğiniz herhangi bir soruya hazırlıklı olun. Ancak verdiğiniz cevap kesinlikle "bu imkansız yapılacak bir şey değil!" olmamalıdır. O problem için uygun farklı çözüm alternatifleri sunmaya çalışmalısınız. + +## Kodunuzda hiçbir şeyi mahvetmeyin + +Yazılımı bir bina gibi düşünün, bir apartmandaki bir pencere kırıldığında ve onarılmadığında, başka bir pencere kırıldığında ve kimse ilgilenmezse, bu böyle devam eder ve sonunda duvarlara grafiti yazılmaya başlar ve bina tamamen düzensiz hale gelir. Sonra; asla kırık bir camla yaşamayın. Bu yazılım için kırık cam örneği, gelecekteki hatalara yol açabilecek kötü tasarımı ve kötü bir kodu içerebilir. + +Aslında bu durum oldukça bilimsel, fizikte entropiyi düşünürseniz, düzensizlik her zaman artmak ister. Bunu engellememek size bağlıdır. Yazılımda bir hata varsa, yeni bir özellik eklemeye odaklanmaktan önce onu düzeltmeye odaklanın ve devam edin. Bunu yapamam dediğinizde değil, yaptım dediğinizde durun. + +## Mükemmeliyetçi olmayın + +Harika bir fikriniz varsa, üzerinde bir yıl veya iki yıl çalıştıktan ve yayınladıktan sonra, geri bildirimlerden öğrenmek çok hayal kırıklığı olabilir ve gerçekten değersiz bir fikir olduğunu öğrenmek. Herhangi bir projede kusursuz ilerlemeden önce, o projenin temel özelliklerine sahip bir MVP'ye sahip olmak ve onu yayınlamak, geri bildirimleri değerlendirmek ve her şey yolundaysa ilerlemek çok daha mantıklıdır. + +Kısacası, mükemmeliyetçilik genellikle yazılım için işe yaramaz. Üründen emin olmak ve sonra onu mükemmelleştirmek daha iyi bir fikirdir. Sonuç olarak, dünya her gün değişiyor ve her dakika yeni teknolojiler ve ürünler ortaya çıkıyor! + +## Varlığınız bilginizdir + +Bilginiz ve deneyiminiz en önemli varlığınızdır. Ancak bu varlıklar zamanla eskimiş olabilir, özellikle teknoloji dünyasında, her şey çok hızlı bir şekilde değişiyor. Bir yıl önce sahip olduğunuz değerli varlıklar şimdi değersiz olabilir. Bu yüzden güncel kalmaya odaklanmalısınız. + +Bir bilgi ve deneyim portföyünü yönetmek, finansal varlıkları yönetmek gibi belirli gereksinimlere sahiptir; + +1. Ciddi yatırımcılar alışkanlık olarak düzenli olarak yatırım yaparlar: Düzenli olarak yatırım yaparsanız, ne kadar az veya çok kazanırsanız kazanın, toplamda büyük bir kar elde edersiniz. + +2. Çeşitlendirme uzun vadeli başarının anahtarıdır: Ne kadar fazla teknolojiye hakim olursanız, değişime o kadar iyi adapte olabilirsiniz. + +3. Akıllı yatırımcılar portföylerini düşük risk, düşük getiri ve yüksek risk, yüksek getiri yatırımları arasında dengelemektedir: Tüm varlığınızı (zaman ve çaba) gelecekte değerli olacağını düşündüğünüz yüksek getirili bir varlığa yatırmak yerine, varlığınızı güvenli dall + +ara dalın ve varlığınızı aynı kutuya koymayın. + +4. Yatırımcılar maksimum getiri için düşük al ve yüksek satın: Gelecekte değer kazanacağını düşündüğünüz varlıklara yatırım yapmak büyük getiriler sağlayabilir. + +5. Portföyler periyodik olarak gözden geçirilmeli ve yeniden dengelenmelidir: Çok dalgalı bir endüstride iş yapıyoruz, bu yüzden yatırımlarınızı düzenli olarak gözden geçirmeli ve değer kaybeden varlıklara dikkat etmelisiniz. + +Ancak tüm bu hedeflerin en önemlisi ve vazgeçilmezi düzenli olarak yatırım yapmaktır. Yatırım yapmazsanız, nereye yatırım yaparsanız yapmanızın bir önemi yoktur. Bu hedeflere ulaşmanın bir aracı olarak kullanabileceğiniz birkaç araç aşağıda verilmiştir. + +1. Her yıl en az bir yeni dil öğrenin. Farklı diller aynı sorunları farklı yollarla çözer. Farklı yaklaşımları öğrenmek düşünme yeteneğinizi genişletebilir ve rutine sıkışıp kalmaktan kaçınmanıza yardımcı olabilir. + +2. Her çeyrek teknik bir kitap okuyun. Alışkanlık kazandıktan sonra, ayda bir kitap okuyun. Kullanmakta olduğunuz teknolojileri öğrendikten sonra, farklı dallarda çalışın ve ilgisiz dallarda çalışın. + +3. Teknik olmayan kitaplar da okuyun. Bilgisayarlar insanlar içindir ve insanları daha iyi anlarsanız, onların çözümlerine çok daha iyi yaklaşabilirsiniz. + +4. Yerel kullanıcı gruplarına katılın. Sadece gitmeyin ve dinlemeyin, aktif olarak katılın. İzolasyon kariyerinize ölümcül olabilir. + +Bu teknolojilerden herhangi birini bir projede kullansanız veya hatta özgeçmişinize koyduğunuzda önemli değil. Bunları öğrenmek öğrenme yeteneğinizi artırır ve problem çözme yeteneğinize katkı sağlar. + +Bir sorunla karşılaşırsanız ve bunun cevabını bilmiyorsanız, bu sorunu bir hedef olarak seçin ve öğrenmeye çalışın. Etrafınızda bir guruya sorun, etrafınızda yoksa çevrimiçi bir tane bulun. Bu hem sorunu yanıtlayacak hem de ağınızı genişletecektir. Sürekli olarak öğrenmeye devam edin. Otobüste seyahat ederken okuyacak bir şey taşıyın ve değerlendirin. + +Bir kitapçının size pazarladığı ürünün en iyi ürünü olmadığı gibi, arama motorlarında ilk sıralara çıkan içeriğe güvenmeyin, doğruluğunu düşünün ve kafanızda tartın ve sorgusuz sualsiz kabul etmeyin. + +## Topluluğunuzu geliştirin! + +Etkili iletişiminiz olmadığında iyi bir fikir yetimdir. Geliştiriciler olarak, son kullanıcılarla iletişim kurarken veya belgeler oluştururken, patronlarımız veya üstlerimizle iletişim kurarken iletişim becerilerimizi kullanıyoruz. İletişim becerilerinizi güçlendirmenin birkaç yolu şunlardır; + +1. Bir programcı olarak göreviniz kelime oyunları oynamak değil, istediğiniz bilgileri doğru ve net bir şekilde iletmektir. Ne demek istediğinizi planlayın. Bir taslak yazın. Sonra kendinize sorun, "Bu söylemek istediğim her şeyi kapsıyor mu?" Bu gereksinimi karşılayana kadar tekrarlayın. Bu planı sadece bir belge yazarken değil, toplantılar ve konuşmalar öncesinde, hatta önemsediğiniz müşterilerle buluşmadan önce yapın. + +2. Bilgi aktarabiliyorsanız, doğru iletişimi sağlayabilirsiniz. Bu nedenle, diğer kişinin karşılayabileceği bilgi seviyesini belirlemelisiniz. + +3. İletişimin çeşitli yolları vardır. Bazı müşteriler e-posta ile bilgilendirilmek isteyebilirken, diğerleri telefonla aranmak isteyebilir. + +4. İnsanların sizi dinlemesini istiyorsanız, yapmanız gereken ilk şey onları dinlemektir. Bir toplantıyı diyaloga dönüştürebilirseniz, bundan daha iyisi yoktur. + +5. İnsanları bekletmeyin, size geri döneceğim, en kısa sürede cevaplayacağım gibi şeyler söyleyerek insanları bekletmeyin. + + +--- + +## Sonuç + +Okuduğunuz için teşekkürler! 🎉 Yazılım geliştirme dünyasındaki en son güncellemeleri ve düşüncelerimi kaçırmayın. Bağlantıdan beni takip ederek [@berkslv](https://x.com/berkslv) ile bağlantıda ve iletişimde kalın. \ No newline at end of file diff --git a/content/turkish/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md b/content/turkish/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md new file mode 100644 index 0000000..8cd2656 --- /dev/null +++ b/content/turkish/posts/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots.md @@ -0,0 +1,454 @@ ++++ +title = "Zero downtime ile Deployment: Azure DevOps Deployment Slots Kullanarak Azure App Service Deployment işlemi" +date = "2023-05-21T00:00:00+03:00" +author = "Berk Selvi" +authorTwitter = "berkslv" #do not include @ +keywords = ["ci/cd","azure devops","azure app service",".NET","azure"] +description = "Azure App Service'te tek örnek olarak çalışan uygulamamızı Azure DevOps pipeline ile deploy ederken büyük olasılıkla birkaç saniyelik kesinti yaşanacaktır. Çünkü uygulama…" +showFullContent = false +readingTime = true ++++ + +Selamlar! Azure App Service'te tek instance olarak çalışan uygulamamızı Azure DevOps işlem hatlarını kullanarak dağıtırken büyük olasılıkla birkaç saniyelik kesinti yaşanacaktır. Çünkü tek instance olarak çalışan uygulamanın yeni sürüme güncellenmesi için yeniden başlatılması gerekecektir. En kötü senaryoda, uygulama sürümlerimizin geçişinde bir hata oluşması durumunda geri alma nedeniyle kesinti süresi uzayacaktır. + +Bu özel sorunu app servisi için deployment slot özelliğini kullanarak çözebiliriz. Bu özellikle, genellikle ayrı örnekler olarak aynı app servis planında çalışan iki farklı örnek, genellikle staging ve production uygulamaları, birbirinden ayrı örnekler olarak çalışır ve production ortamına geçiş yapıldığında app servisi tarafından değiştirme işlemi yönetilir ve kesinti yaşanmaz. + +Bu yazıda, çok basit bir Dotnet Web API projesi oluşturacak, Azure DevOps Repository kullanarak barındıracak, Azure DevOps pipeline ile CI/CD pipeline oluşturarak Azure App Service'e dağıtacak ve deployment slot özelliği ile kesintisiz deployment yapacağız. Aşağıdaki adımları takip ederek süreci 5 adımda tamamlayacağız. Başlayalım. + +1. Uygulamanın Oluşturulması +2. Azure DevOps Repo +3. Azure DevOps build pipeline +4. Azure App service +5. Zero downtime testing + +## Uygulamanın Oluşturulması + +Deploy etmek için basit bir Dotnet Web API projesi oluşturuyoruz. Bunun için aşağıdaki komutları kullanıyoruz. + +```bash +mkdir backend +cd ./backend +dotnet new sln -n Slot +dotnet new webapi -n Slot.API +dotnet sln add ./Slot.API/ +``` +properties/launchSetting.json dosyasındaki _profiles.http_ özelliğini aşağıdaki gibi güncelliyoruz. Burada yalnızca _applicationUrl_ ve _launchBrowser_ özelliklerini güncelledik. + +```json +// ... + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5050", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, +// ... +``` + +Aşağıdaki güncellemeyi Program.cs dosyasında yaparak, uygulamanın erişilebilir ve /health uç noktasına yapılan isteklere yanıt verebileceğini test edeceğiz. Uygulamanızda bir veritabanı kullanıyorsanız, [AddDbContextCheck](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.dependencyinjection.entityframeworkcorehealthchecksbuilderextensions.adddbcontextcheck?view=dotnet-plat-ext-8.0) yöntemi ile veritabanına erişimde bir sorun olup olmadığını da test edebilirsiniz. + +```cs +var builder = WebApplication.CreateBuilder(args); + +// ... + +builder.Services.AddHealthChecks(); + +var app = builder.Build(); + +// ... + +// app.UseHttpsRedirection(); + +app.MapControllers(); + +app.UseHealthChecks("/health"); + +app.Run(); +``` + +Uygulamamız için bu kadar! Şimdi şu komutları çalıştırarak localhost:5050/health adresine erişebiliriz. + +```bash +cd ./Slot.API +dotnet run +curl -4 http://localhost:5050/health +# Healthy +``` + +Daha sonra uygulamamızı dağıtmak için Docker kullanacağız, bu yüzden Dockerfile dosyamızı sln dosyamızla aynı dizine koyuyoruz. + +```Dockerfile +# Build Stage +FROM mcr.microsoft.com/dotnet/aspnet:7.0-alpine AS base +WORKDIR /app +EXPOSE 8080 + + +# Publish Stage +FROM mcr.microsoft.com/dotnet/sdk:7.0-alpine AS build +COPY ["Slot.API/Slot.API.csproj", "Slot.API/"] +RUN dotnet restore "Slot.API/Slot.API.csproj" +COPY . . +WORKDIR "/Slot.API" +RUN dotnet build "Slot.API.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Slot.API.csproj" -c Release -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENV ASPNETCORE_URLS=http://*:8080 +ENV ASPNETCORE_ENVIRONMENT=Production +ENTRYPOINT ["dotnet", "Slot.API.dll"] +``` + +Aşağıdaki iki komutu kullanarak imajı başarıyla oluşturup container'ı build edip çalıştırabiliriz. + + +```bash +docker build -t deployment-slots-demo . +docker run -it -p 80:8080 deployment-slots-demo -n deployment-slots-demo-container +``` + +## Uygulamanın Çalışıyor Olduğunu Doğrulayın + + +Önceki adımda sağlık kontrolü özelliği ile uygulamamızı oluşturduktan sonra, bu adrese her saniye bir istek yaparak uygulamanın erişilebilir olduğunu doğrulayabiliriz. Bu şekilde, Azure'a dağıtıldığında erişilebilir olduğunu doğrulamış olacağız. + +Bu bölümde, uygulamanın her 50 milisaniyede bir istek yaparak erişilebilir olup olmadığını doğrulamak için basit bir Node.js script'i yazacağız. Bunun için aşağıdaki komutları kullanıyoruz. + +```bash +mkdir health-check +npm init -y +touch index.js +npm install node-fetch +``` + +Aşağıdaki kısmı package.json dosyasına ekliyoruz. + +```json +// ... +"scripts": { + "start": "node index.js" +}, +"type": "module", +// ... +``` + +index.js dosyamızı aşağıdaki gibi oluşturabiliriz. Bu kod ile belirtilen url'e her 50 milisaniyede bir istek yapacak ve yanıtı konsola yazacaktır. + +```js +import fetch from "node-fetch"; + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("http://localhost:5050/health"); + }, 50); + setInterval(() => { + check("http://localhost:5050/health"); + }, 50); +})(); +``` + +API projemizde UseHttpsRedirection middleware'ini kapatmazsanız, geçersiz bir SSL sertifikası hatası alabilirsiniz. Bunu aşağıdaki gibi düzeltebilirsiniz. + +```js +import fetch from "node-fetch"; +import https from "https"; + +const httpsAgent = new https.Agent({ + rejectUnauthorized: false, +}); + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + agent: httpsAgent, + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("https://localhost:5051/health"); + }, 50); + setInterval(() => { + check("https://localhost:5051/health"); + }, 50); +})(); +``` + +## Azure DevOps Repo + +Azure DevOps hesabımıza giriş yapıyoruz ve aşağıdaki gibi yeni bir repo oluşturuyoruz. + +Azure deployment + +
+ +Azure deployment + +Bu repo'yu bilgisayarımıza klonluyoruz, yazdığımız kodu bu repo klasörüne taşıyoruz ve kodları origin'e iletiyoruz. + +Azure deployment + +```bash +git add . +git commit -m "inital commit" +git push origin +``` + +Azure deployment + +## Azure DevOps build pipeline + +Pipeline ekranında, en sağ üstteki New pipeline düğmesine tıklıyoruz. Ardından aşağıdaki adımları izleyerek docker dosyasını oluşturup ve Azure Container Registry'e iteleriyoruz. + + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +Şimdi Azure Container Registry'de image deployment işlemimizin başarılı olduğunu kontrol ediyoruz. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +Görüldüğü gibi, boru hattı çalıştırıldığında, Docker image'i başarıyla Azure Container Registry'de oluşturulmuş ve kullanılmak üzere bizi bekliyor. Bu ayarların ardından, main branch'de /backend dizininde yapılan her değişiklik ile tetiklenerek yeni bir Docker image'i oluşturulacak. + +## Azure App service + +Azure App servisi, güvenlik, yük dengeleme, otomatik ölçeklendirme gibi özellikleriyle Azure tarafından yönetilen web uygulamalarını dağıtmamıza olanak tanır. Azure app servisleri ekranı üzerinden yeni bir app servisi oluşturabiliriz. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +App servisini oluşturduktan sonra, Container Registry şifresini yapılandırma sekmesinden güncellemeniz gerekebilir. + +## Azure DevOps release pipeline + +release pipeline ile, build pipeline tarafından oluşturulan Docker image'i kullanarak App servisine deploy edilir ve uygulamamızı yayınlamış oluruz. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +Staging dağıtımına girdiğimizde, staging versiyonumuz için dağıtım alırız ve Docker image'inde yaptığımız değişikliklerin canlı hale gelmesini sağlamak için uygulama servisimizi yeniden başlatırız. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +production aşamasında, app service'de bir deployment yapmıyoruz, bunun yerine staging aşamasıyla değişim işlemini gerçekleştiriyoruz ve bu şekilde, production erişiminde herhangi bir kesinti yaşamadan staging'de çalışan uygulamamızı production ile değiştirdiğimiz için production uygulamamıza erişebiliyoruz. + +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +
+ +Azure deployment + +Şimdi, main bracnh'te yapılan herhangi bir değişiklik önce build pipeline (CI) ile geçecek, ardından release pipeline (CD) geçecek ve release, staging ortamına yapılacak. Production ortamına geçmek isterseniz, production deployment işlemi manuel olarak release ekranından tetiklenmelidir. + +Azure deployment + +## Zero downtime testing + +Uygulamamız için gerekli pipeline'ları başarıyla oluşturduk, bundan sonra release sırasında herhangi bir kesinti olup olmadığını test etmemiz gerekecek. Bunun için _health-check/index.js_ dosyamızı aşağıdaki gibi güncelliyorum ve uygulamayı npm run start komutuyla çalıştırıp pipeline'ı tetikliyorum. Ardından, konsolda herhangi bir hata mesajı almadan, yani herhangi bir kesinti olmadan deployment sürecini tamamlıyorum! + +```js +import fetch from "node-fetch"; + +const check = async (url) => { + try { + const response = await fetch(url, { + method: "GET", + }); + const result = await response.text(); + + if (result !== "Healthy") { + console.log(`${new Date().toISOString()}, ${url} result is not OK`); + } + } catch (error) { + console.log(`${new Date().toISOString()}, ${url} error is ${error.message}`); + } +}; + +(() => { + setInterval(() => { + check("https://deployment-slot-demo.azurewebsites.net/health"); + }, 50); + setInterval(() => { + check("https://deployment-slot-demo-staging.azurewebsites.net/health"); + }, 50); +})(); +``` + +Benzer bir işlemi deployment slots özelliği uygulamayan bir App Service ile denersek, aşağıdaki gibi bir hata alırız. + +```bash +2023-11-14T12:28:39.506Z, [some-url]/health error is request to [some-url]/health failed, reason: connect ETIMEDOUT 100.100.100.100:443 +``` + +--- + +## Sonuç + +Okuduğunuz için teşekkürler! 🎉 Yazılım geliştirme dünyasındaki en son güncellemeleri ve düşüncelerimi kaçırmayın. Bağlantıdan beni takip ederek [@berkslv](https://x.com/berkslv) ile bağlantıda ve iletişimde kalın. \ No newline at end of file diff --git a/content/turkish/posts/the-future-of-backend-development-predictions-and-trends.md b/content/turkish/posts/the-future-of-backend-development-predictions-and-trends.md deleted file mode 100644 index 59b1f35..0000000 --- a/content/turkish/posts/the-future-of-backend-development-predictions-and-trends.md +++ /dev/null @@ -1,41 +0,0 @@ -+++ -title = "The Future of Backend Development: Predictions and Trends" -date = "2023-01-10T11:01:10+03:00" -author = "Berk Selvi" -authorTwitter = "berkslv" #do not include @ -tags = ["backend", "container", "cloud", "ai development"] -keywords = ["backend", "container", "cloud", "ai development"] -description = "Take a closer look at what the future of backend development might look like, and consider some of the trends and predictions that experts are making." -showFullContent = false -readingTime = true -+++ - -# The Future of Backend Development: Predictions and Trends - - -As a backend developer, you are always looking for ways to improve your skills and stay ahead of the curve. One way to do this is to keep an eye on the future of backend development and stay up-to-date with the latest trends and predictions. In this blog post, we'll take a closer look at what the future of backend development might look like, and consider some of the trends and predictions that experts are making. - - -## Cloud - -One of the biggest trends in backend development is the increasing use of cloud computing. Cloud computing allows developers to build and run applications and services on remote servers, rather than on local servers or personal computers. This has a number of benefits, including scalability, flexibility, and cost-effectiveness. As a result, many experts believe that cloud computing will continue to play a major role in backend development in the future. - - -## Containers - -Another trend in backend development is the growing use of containerization technologies, such as Docker. Containerization allows developers to package their applications and dependencies into lightweight containers that can be easily deployed and run on any platform. This makes it easier to build and deploy applications, and makes it easier to scale them as well. - - -## Microservices - -A third trend in backend development is the increasing use of microservices architecture. In a microservices architecture, an application is broken down into small, independent services that can be developed, deployed, and scaled independently of one another. This makes it easier to build and maintain complex applications, and allows developers to make changes and updates more quickly and easily. - - -## AI powered development - -In addition to these trends, there are also a number of predictions about the future of backend development. One prediction is that machine learning and artificial intelligence (AI) will play a larger role in backend development in the future. For example, machine learning algorithms could be used to optimize the performance of backend systems, or to automate certain tasks that are currently performed manually. Another prediction is that the use of low-code and no-code platforms will increase, making it easier for developers to build and deploy backend systems without having to write a lot of code. - - -## Conclusion - -Overall, it is clear that the future of backend development is bright, with many exciting trends and predictions on the horizon. Whether you are just starting out as a backend developer or are a seasoned pro, it is important to stay up-to-date with these trends and predictions, and to continue learning and growing your skills. By doing so, you can ensure that you are well-prepared for the future of backend development and that you are able to take advantage of the many opportunities that are likely to come your way. diff --git a/layouts/partials/header.html b/layouts/partials/header.html index f8e3ccd..06e786c 100644 --- a/layouts/partials/header.html +++ b/layouts/partials/header.html @@ -1,13 +1,3 @@ - - {{ template "_internal/google_analytics.html" . }} - {{ template "_internal/google_analytics_async.html" . }} -
-
- - {{ if len $.Site.Menus }} - - {{ end }} -
{{ if len $.Site.Menus }} {{ partial "menu.html" . }} {{ end }}
diff --git a/static/css/custom.css b/static/css/custom.css new file mode 100644 index 0000000..d9403ff --- /dev/null +++ b/static/css/custom.css @@ -0,0 +1,136 @@ +body { + border-left: 10px solid var(--accent); + font-size: 1.2em; +} + +.container { + max-width: 1000px; +} + +.menu { + margin: 1px 1px !important; +} + +.footer__inner { + width: 100% !important; +} + +#hero { + margin-top: 30px; + margin-bottom: 80px; + display: flex; + justify-content: space-between; + align-items: flex-start; +} + +#hero .content { + display: flex; + flex-direction: column; + justify-content: space-between; + height: 300px; +} + +#hero .content h1 { + font-size: 3rem; + margin: 0; + color: var(--accent); +} + +#hero .content h2 { + font-size: 1.5rem; + font-weight: 400; + margin-top: 10px; +} + +#hero .content .controls { + width: fit-content; +} + +.contact { + display: inline-flex; + align-items: center; + color: var(--background); + border-radius: 0%; + background-color: var(--accent); + font-weight: 400; + font-size: 1.5rem; + font-family: "Ubuntu", sans-serif; + padding: 10px 20px; + text-decoration: none; +} + +.socials { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 15px; +} + +.socials img { + width: 30px; + height: 30px; +} + +.hero-image { + width: 300px; + height: 300px; + border: 8px solid var(--accent); +} + +.sub-title { + font-size: 1.8rem; + margin-bottom: 10px; + color: var(--accent); +} + +.post-content img { + object-fit: cover; + width: 100%; +} + +.image-sub-title { + text-align: center; + display: block; + font-size: 0.8em; + margin-top: 0; +} + +@media (max-width: 900px) { + #hero .content h1 { + font-size: 2.5rem; + } + + #hero .content h2 { + font-size: 1.2rem; + } + + .contact { + font-size: 1.2rem; + } + + .hero-image { + width: 280px; + height: 280px; + } +} + +@media (max-width: 684px) { + #hero { + flex-direction: column; + align-items: center; + } + + #hero .content { + height: 250px; + } + + #hero .content .controls { + margin-top: 20px; + } + + .hero-image { + margin-top: 80px; + width: 350px; + height: 350px; + } +} diff --git a/static/icon/android-icon-144x144.png b/static/icon/android-icon-144x144.png new file mode 100644 index 0000000..abe3ca4 Binary files /dev/null and b/static/icon/android-icon-144x144.png differ diff --git a/static/icon/android-icon-192x192.png b/static/icon/android-icon-192x192.png new file mode 100644 index 0000000..90886a1 Binary files /dev/null and b/static/icon/android-icon-192x192.png differ diff --git a/static/icon/android-icon-36x36.png b/static/icon/android-icon-36x36.png new file mode 100644 index 0000000..2d0349e Binary files /dev/null and b/static/icon/android-icon-36x36.png differ diff --git a/static/icon/android-icon-48x48.png b/static/icon/android-icon-48x48.png new file mode 100644 index 0000000..7248eee Binary files /dev/null and b/static/icon/android-icon-48x48.png differ diff --git a/static/icon/android-icon-72x72.png b/static/icon/android-icon-72x72.png new file mode 100644 index 0000000..32a9274 Binary files /dev/null and b/static/icon/android-icon-72x72.png differ diff --git a/static/icon/android-icon-96x96.png b/static/icon/android-icon-96x96.png new file mode 100644 index 0000000..5ffc998 Binary files /dev/null and b/static/icon/android-icon-96x96.png differ diff --git a/static/icon/apple-icon-114x114.png b/static/icon/apple-icon-114x114.png new file mode 100644 index 0000000..512e241 Binary files /dev/null and b/static/icon/apple-icon-114x114.png differ diff --git a/static/icon/apple-icon-120x120.png b/static/icon/apple-icon-120x120.png new file mode 100644 index 0000000..cf9c04c Binary files /dev/null and b/static/icon/apple-icon-120x120.png differ diff --git a/static/icon/apple-icon-144x144.png b/static/icon/apple-icon-144x144.png new file mode 100644 index 0000000..abe3ca4 Binary files /dev/null and b/static/icon/apple-icon-144x144.png differ diff --git a/static/icon/apple-icon-152x152.png b/static/icon/apple-icon-152x152.png new file mode 100644 index 0000000..a07f8a3 Binary files /dev/null and b/static/icon/apple-icon-152x152.png differ diff --git a/static/icon/apple-icon-180x180.png b/static/icon/apple-icon-180x180.png new file mode 100644 index 0000000..4609b59 Binary files /dev/null and b/static/icon/apple-icon-180x180.png differ diff --git a/static/icon/apple-icon-57x57.png b/static/icon/apple-icon-57x57.png new file mode 100644 index 0000000..71c42e6 Binary files /dev/null and b/static/icon/apple-icon-57x57.png differ diff --git a/static/icon/apple-icon-60x60.png b/static/icon/apple-icon-60x60.png new file mode 100644 index 0000000..c06d2a5 Binary files /dev/null and b/static/icon/apple-icon-60x60.png differ diff --git a/static/icon/apple-icon-72x72.png b/static/icon/apple-icon-72x72.png new file mode 100644 index 0000000..32a9274 Binary files /dev/null and b/static/icon/apple-icon-72x72.png differ diff --git a/static/icon/apple-icon-76x76.png b/static/icon/apple-icon-76x76.png new file mode 100644 index 0000000..cafac4b Binary files /dev/null and b/static/icon/apple-icon-76x76.png differ diff --git a/static/icon/apple-icon-precomposed.png b/static/icon/apple-icon-precomposed.png new file mode 100644 index 0000000..65b3ff0 Binary files /dev/null and b/static/icon/apple-icon-precomposed.png differ diff --git a/static/icon/apple-icon.png b/static/icon/apple-icon.png new file mode 100644 index 0000000..65b3ff0 Binary files /dev/null and b/static/icon/apple-icon.png differ diff --git a/static/icon/browserconfig.xml b/static/icon/browserconfig.xml new file mode 100644 index 0000000..76d3235 --- /dev/null +++ b/static/icon/browserconfig.xml @@ -0,0 +1,11 @@ + + + + + + + + #1D1E28 + + + \ No newline at end of file diff --git a/static/icon/favicon-16x16.png b/static/icon/favicon-16x16.png new file mode 100644 index 0000000..50aae3a Binary files /dev/null and b/static/icon/favicon-16x16.png differ diff --git a/static/icon/favicon-32x32.png b/static/icon/favicon-32x32.png new file mode 100644 index 0000000..c121d9f Binary files /dev/null and b/static/icon/favicon-32x32.png differ diff --git a/static/icon/favicon-96x96.png b/static/icon/favicon-96x96.png new file mode 100644 index 0000000..5ffc998 Binary files /dev/null and b/static/icon/favicon-96x96.png differ diff --git a/static/icon/favicon.ico b/static/icon/favicon.ico new file mode 100644 index 0000000..861f100 Binary files /dev/null and b/static/icon/favicon.ico differ diff --git a/static/icon/manifest.json b/static/icon/manifest.json new file mode 100644 index 0000000..013d4a6 --- /dev/null +++ b/static/icon/manifest.json @@ -0,0 +1,41 @@ +{ + "name": "App", + "icons": [ + { + "src": "\/android-icon-36x36.png", + "sizes": "36x36", + "type": "image\/png", + "density": "0.75" + }, + { + "src": "\/android-icon-48x48.png", + "sizes": "48x48", + "type": "image\/png", + "density": "1.0" + }, + { + "src": "\/android-icon-72x72.png", + "sizes": "72x72", + "type": "image\/png", + "density": "1.5" + }, + { + "src": "\/android-icon-96x96.png", + "sizes": "96x96", + "type": "image\/png", + "density": "2.0" + }, + { + "src": "\/android-icon-144x144.png", + "sizes": "144x144", + "type": "image\/png", + "density": "3.0" + }, + { + "src": "\/android-icon-192x192.png", + "sizes": "192x192", + "type": "image\/png", + "density": "4.0" + } + ] +} \ No newline at end of file diff --git a/static/icon/ms-icon-144x144.png b/static/icon/ms-icon-144x144.png new file mode 100644 index 0000000..abe3ca4 Binary files /dev/null and b/static/icon/ms-icon-144x144.png differ diff --git a/static/icon/ms-icon-150x150.png b/static/icon/ms-icon-150x150.png new file mode 100644 index 0000000..c6330fc Binary files /dev/null and b/static/icon/ms-icon-150x150.png differ diff --git a/static/icon/ms-icon-310x310.png b/static/icon/ms-icon-310x310.png new file mode 100644 index 0000000..b371a1a Binary files /dev/null and b/static/icon/ms-icon-310x310.png differ diff --git a/static/icon/ms-icon-70x70.png b/static/icon/ms-icon-70x70.png new file mode 100644 index 0000000..35ceaa5 Binary files /dev/null and b/static/icon/ms-icon-70x70.png differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-1.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-1.webp new file mode 100644 index 0000000..0aee890 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-1.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-10.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-10.webp new file mode 100644 index 0000000..93a7c40 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-10.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-11.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-11.webp new file mode 100644 index 0000000..b1d5304 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-11.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-12.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-12.webp new file mode 100644 index 0000000..8a6e185 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-12.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-13.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-13.webp new file mode 100644 index 0000000..7c89bfb Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-13.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-14.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-14.webp new file mode 100644 index 0000000..3645e2b Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-14.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-15.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-15.webp new file mode 100644 index 0000000..fe92d6a Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-15.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-16.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-16.webp new file mode 100644 index 0000000..78a2aff Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-16.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-17.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-17.webp new file mode 100644 index 0000000..627edd6 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-17.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-18.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-18.webp new file mode 100644 index 0000000..df20555 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-18.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-19.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-19.webp new file mode 100644 index 0000000..74331c3 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-19.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-2.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-2.webp new file mode 100644 index 0000000..cb264ee Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-2.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-20.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-20.webp new file mode 100644 index 0000000..76d4758 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-20.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-21.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-21.webp new file mode 100644 index 0000000..994a575 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-21.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-22.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-22.webp new file mode 100644 index 0000000..173c882 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-22.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-23.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-23.webp new file mode 100644 index 0000000..b2397ac Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-23.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-24.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-24.webp new file mode 100644 index 0000000..66aaa85 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-24.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-25.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-25.webp new file mode 100644 index 0000000..c849646 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-25.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-26.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-26.webp new file mode 100644 index 0000000..efaf032 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-26.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-27.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-27.webp new file mode 100644 index 0000000..5dab3ac Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-27.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-28.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-28.webp new file mode 100644 index 0000000..b8d946f Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-28.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-29.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-29.webp new file mode 100644 index 0000000..3d15761 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-29.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-3.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-3.webp new file mode 100644 index 0000000..be1684f Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-3.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-30.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-30.webp new file mode 100644 index 0000000..b36a065 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-30.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-31.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-31.webp new file mode 100644 index 0000000..dbcc5f7 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-31.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-32.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-32.webp new file mode 100644 index 0000000..fc4e4d2 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-32.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-33.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-33.webp new file mode 100644 index 0000000..59418bc Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-33.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-34.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-34.webp new file mode 100644 index 0000000..bca82d1 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-34.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-35.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-35.webp new file mode 100644 index 0000000..f30a2bd Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-35.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-36.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-36.webp new file mode 100644 index 0000000..2629c88 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-36.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-37.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-37.webp new file mode 100644 index 0000000..b9ff9a1 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-37.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-38.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-38.webp new file mode 100644 index 0000000..44192e0 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-38.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-39.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-39.webp new file mode 100644 index 0000000..b1166c6 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-39.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-4.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-4.webp new file mode 100644 index 0000000..03b7ef6 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-4.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-40.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-40.webp new file mode 100644 index 0000000..38e712e Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-40.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-41.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-41.webp new file mode 100644 index 0000000..5a7d2c6 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-41.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-42.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-42.webp new file mode 100644 index 0000000..0bd04fd Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-42.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-43.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-43.webp new file mode 100644 index 0000000..4450de8 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-43.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-44.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-44.webp new file mode 100644 index 0000000..99ff76e Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-44.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-45.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-45.webp new file mode 100644 index 0000000..13d6d28 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-45.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-46.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-46.webp new file mode 100644 index 0000000..0d7a659 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-46.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-5.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-5.webp new file mode 100644 index 0000000..caee8f8 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-5.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-6.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-6.webp new file mode 100644 index 0000000..224ccea Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-6.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-7.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-7.webp new file mode 100644 index 0000000..417d844 Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-7.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-8.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-8.webp new file mode 100644 index 0000000..5da975b Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-8.webp differ diff --git a/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-9.webp b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-9.webp new file mode 100644 index 0000000..7817f8e Binary files /dev/null and b/static/img/achieving-zero-downtime-azure-app-service-deployment-using-azure-devops-and-deployment-slots/azure-deployment-9.webp differ diff --git a/static/img/background-jobs-and-hangife-in-net/hangfire-dashboard.webp b/static/img/background-jobs-and-hangife-in-net/hangfire-dashboard.webp new file mode 100644 index 0000000..7aa3a30 Binary files /dev/null and b/static/img/background-jobs-and-hangife-in-net/hangfire-dashboard.webp differ diff --git a/static/img/berk-selvi.jpeg b/static/img/berk-selvi.jpeg new file mode 100644 index 0000000..a75ca7d Binary files /dev/null and b/static/img/berk-selvi.jpeg differ diff --git a/static/img/github.svg b/static/img/github.svg old mode 100644 new mode 100755 index bc1986f..e719f60 --- a/static/img/github.svg +++ b/static/img/github.svg @@ -1,4 +1,10 @@ - - - - \ No newline at end of file + + + + + + + + + + diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm-confirmation.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm-confirmation.webp new file mode 100644 index 0000000..2e45082 Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm-confirmation.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm.webp new file mode 100644 index 0000000..b158f7f Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/create-realm.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-1.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-1.webp new file mode 100644 index 0000000..1bd2927 Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-1.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-2.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-2.webp new file mode 100644 index 0000000..f3d741f Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-2.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-3.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-3.webp new file mode 100644 index 0000000..a45f79c Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-3.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-4.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-4.webp new file mode 100644 index 0000000..59d5f9b Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-4.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-5.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-5.webp new file mode 100644 index 0000000..78f1ff4 Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-5.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-6.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-6.webp new file mode 100644 index 0000000..2fc499d Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-6.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-7.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-7.webp new file mode 100644 index 0000000..8c174eb Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-7.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-8.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-8.webp new file mode 100644 index 0000000..aa86825 Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-8.webp differ diff --git a/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-9.webp b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-9.webp new file mode 100644 index 0000000..67ef630 Binary files /dev/null and b/static/img/how-to-secure-dotnet-vue-application-with-keycloak/keycloak-9.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Exposed-path-recommendations.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Exposed-path-recommendations.webp new file mode 100644 index 0000000..41e4de6 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Exposed-path-recommendations.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Microservice-architecture-with-Ocelot-and-Keycloak.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Microservice-architecture-with-Ocelot-and-Keycloak.webp new file mode 100644 index 0000000..24c7cf2 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/Microservice-architecture-with-Ocelot-and-Keycloak.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-1.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-1.webp new file mode 100644 index 0000000..df1648d Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-1.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-2.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-2.webp new file mode 100644 index 0000000..1965cfc Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-2.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-3.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-3.webp new file mode 100644 index 0000000..e671e38 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-3.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-4.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-4.webp new file mode 100644 index 0000000..fc96f2d Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-4.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-5.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-5.webp new file mode 100644 index 0000000..12eb100 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-5.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-6.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-6.webp new file mode 100644 index 0000000..0677a38 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-6.webp differ diff --git a/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-7.webp b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-7.webp new file mode 100644 index 0000000..97afc93 Binary files /dev/null and b/static/img/how-to-use-ocelot-and-keycloak-together-to-secure-microservices-from-api-gateway/keycloak-7.webp differ diff --git a/static/img/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net/rsa-encryption.webp b/static/img/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net/rsa-encryption.webp new file mode 100644 index 0000000..31befbe Binary files /dev/null and b/static/img/how-to-use-rsa-for-encryption-in-javascript-and-decryption-in-net/rsa-encryption.webp differ diff --git a/static/img/instagram.svg b/static/img/instagram.svg deleted file mode 100644 index 1054dea..0000000 --- a/static/img/instagram.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/static/img/linkedin.svg b/static/img/linkedin.svg new file mode 100755 index 0000000..5893f0f --- /dev/null +++ b/static/img/linkedin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/static/img/logo.svg b/static/img/logo.svg new file mode 100644 index 0000000..e06d5ca --- /dev/null +++ b/static/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/img/mail.svg b/static/img/mail.svg new file mode 100755 index 0000000..a976623 --- /dev/null +++ b/static/img/mail.svg @@ -0,0 +1,4 @@ + + + + diff --git a/static/img/medium.svg b/static/img/medium.svg deleted file mode 100644 index d09a97c..0000000 --- a/static/img/medium.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/static/img/send.svg b/static/img/send.svg new file mode 100644 index 0000000..cc10814 --- /dev/null +++ b/static/img/send.svg @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/static/img/twitter.svg b/static/img/twitter.svg old mode 100644 new mode 100755 index 49f975c..ecf68f8 --- a/static/img/twitter.svg +++ b/static/img/twitter.svg @@ -1,4 +1,3 @@ - - - - \ No newline at end of file + + + diff --git a/themes/terminal/assets/css/font.css b/themes/terminal/assets/css/font.css index 6bc5f52..a7f522e 100644 --- a/themes/terminal/assets/css/font.css +++ b/themes/terminal/assets/css/font.css @@ -1,17 +1,17 @@ @font-face { font-display: swap; - font-family: 'Fira Code'; + font-family: 'Ubuntu'; font-style: normal; font-weight: 400; - src: url("../fonts/FiraCode-Regular.woff") format("woff"); + src: url("../fonts/Ubuntu-Regular.woff") format("woff"); font-display: swap; } @font-face { font-display: swap; - font-family: 'Fira Code'; + font-family: 'Ubuntu'; font-style: normal; font-weight: 800; - src: url("../fonts/FiraCode-Bold.woff") format("woff"); + src: url("../fonts/Ubuntu-Bold.woff") format("woff"); font-display: swap; } diff --git a/themes/terminal/assets/css/footer.css b/themes/terminal/assets/css/footer.css index bac4ec0..dd84422 100644 --- a/themes/terminal/assets/css/footer.css +++ b/themes/terminal/assets/css/footer.css @@ -33,7 +33,7 @@ } & > *:first-child:not(:only-child) { - margin-right: 10px; + /*margin-right: 10px;*/ @media (--tablet) { border: none; diff --git a/themes/terminal/assets/css/main.css b/themes/terminal/assets/css/main.css index 8929ea1..59e3013 100644 --- a/themes/terminal/assets/css/main.css +++ b/themes/terminal/assets/css/main.css @@ -11,7 +11,7 @@ html { body { margin: 0; padding: 0; - font-family: 'Fira Code', Monaco, Consolas, Ubuntu Mono, monospace; + font-family: 'Ubuntu', sans-serif; font-size: 1rem; line-height: 1.54; letter-spacing: -0.02em; @@ -128,13 +128,13 @@ figure { } code, kbd { - font-family: 'Fira Code', Monaco, Consolas, Ubuntu Mono, monospace !important; + font-family: 'Ubuntu', sans-serif !important; font-feature-settings: normal; background: color-mod(var(--accent) a(20%)); color: var(--accent); padding: 1px 6px; margin: 0 2px; - font-size: .95rem; + font-size: 1rem; code, kbd { background: transparent; @@ -147,7 +147,7 @@ pre { background: transparent !important; padding: 20px 10px; margin: 40px 0; - font-size: .95rem !important; + font-size: 1rem !important; overflow: auto; border-top: 1px solid rgba(255, 255, 255, .1); border-bottom: 1px solid rgba(255, 255, 255, .1); diff --git a/themes/terminal/assets/fonts/Ubuntu-Bold.woff b/themes/terminal/assets/fonts/Ubuntu-Bold.woff new file mode 100644 index 0000000..78a0928 Binary files /dev/null and b/themes/terminal/assets/fonts/Ubuntu-Bold.woff differ diff --git a/themes/terminal/assets/fonts/Ubuntu-Regular.woff b/themes/terminal/assets/fonts/Ubuntu-Regular.woff new file mode 100644 index 0000000..7d53b58 Binary files /dev/null and b/themes/terminal/assets/fonts/Ubuntu-Regular.woff differ diff --git a/themes/terminal/layouts/_default/index.html b/themes/terminal/layouts/_default/index.html index 1cab615..cc4dfb5 100644 --- a/themes/terminal/layouts/_default/index.html +++ b/themes/terminal/layouts/_default/index.html @@ -21,7 +21,7 @@