This project is part of the Harck CMS by Harckade - A free and opensource serverless content management system
This repository represents the backend part of the backoffice, powered by Azure functions, and it is meant to be deployed alongside with the Harck CMS FE (frontend).
You can find full API specification on the swagger
file
Warning
Harckade and Harck CMS team is not associated with any entity that is not listed on Harckade official website nor responsible for any damage/content that those entities may produce. Harckade is also not responsible for any abuse of local or global laws or policies that may result from malicious actors that use Harckade's technology.
- Microsoft Azure account
- Setup a Microsoft Entra ID (formerly known as Azure Active Directory) tenant. You can follow this guide
- Create an
App registration
as a Single-page app. You can follow this guide - On the created App registration, create
App roles
following this guide, with respective keys and values:"Display name" = "Administrator" "Value" = "administrator" "Description" = "CMS portal administrator"
"Display name" = "Editor" "Value" = "editor" "Description" = "CMS portal editor. Can do everything an admin can except add/delete users"
"Display name" = "Viewer" "Value" = "viewer" "Description" = "CMS portal viewer. Cannot edit - View only"
- On the left panel, under the
Manage
section go to theAuthentication
tab and add a Single-page application platform.- If this is your development instance you may want to add
http://localhost:3000
as your redirect URI - For production website use your static web app URI
- Under the
Implicit grant and hybrid flows
section make sure that onlyAccess tokens (used for implicit flows)
is checked. - On the
Advanced settings
make sure that you do not allow the usage ofpublic client flows
by selecting the optionNo
.No
should be highlighted.
- If this is your development instance you may want to add
- On the left panel, under the
Manage
section go to theCertificates & secrets
tab and generate a set of credentials with a desired expiration (You will need them later on the next sections of this guide). You can name it anything you want, for example: "harckade_credentials" - On the left panel, under the
Manage
section go to theToken configuration
tab and add anoptional claim
ofaccess
type - email (The addressable email for this user, if the user has one) - On the left panel, under the
Manage
section go to theExpose an API
tab and add a new scope:- Scope name: api
- who can consent?: Admins and users
- Admin consent display name: Harck CMS
- Admin consent description: Authorize the usage of Microsoft Entra ID on Harck CMS
- State: Enabled
- On the left panel, under the
Manage
section go to theAPI permissions
tab and add the following permissions, by clicking on theAdd a permission
button:
- select
My API
tab- api (Harck CMS)
- add permission
- Microsoft Graph
- Delegated permissions:
- offline_access
- openid
- profile
- User.Read
- Application permissions:
- Application.Read.All
- AppRoleAssignment.ReadWrite.All
- User.Invite.All
- User.Read.All
- User.ReadWrite.All
- Then, click on the
Grant admin consent for "YOUR_APP_REGISTRATION_NAME"
- Delegated permissions:
- Navigate back to the main Azure portal page and open
Microsoft Entra ID
. Then, on the left panel click on theEnterprise applications
.- You should have, at least, one application for Harck CMS with the same name as your app registration
- Open it and navigate to the
Users and groups
tab - Click on the
Add user/group
button, select your user and then theadministrator
role and hit theAssign
button
- From your App registration save the
Directory (tenant) ID
and theApplication (client) ID
- Amazon Web Services account (Required for newsletter and contact form functionality)
- Email provider that allows custom domains
What you need to do before you can procceed with the next steps?
- Clone this repository
- Setup a new Github personal access token
- Make sure you completed all steps from Global requirements and your Microsoft Entra ID tenant is properly configured
Create Amazon SES SMTP (Email) resource - newsletter will be send through this service. Follow official AWS documentation
- Your email provider must allow you to use custom domains - usually this is a paid feature (Proton, Google, Outlook). There are some free options, such as Zoho Mail, that you can find by searching the web.
- Configure your server DNS with Amazon SES MX and TXT records. Follow this guide
- Setup DMARC DKIM and SPF. Follow this guide
- Make sure you configured everything properly by scanning your DNS records. You can use a tool such as MxToolbox to do it.
Create a new SignalR resource
- Select your desired subscription. For example,
Pay-As-You-Go
- Give any valid name for the resource name
- For the
Region
select the region that is nearest to you - For the
Pricing tier
, click onchange
and then selectFree
(it should be more than enough for you to start a blog with a couple of people as administrators/editors, you can always revisit it later and create a more powerfull resource) - For the
Service mode
selectServerless
- On the
Networking
tab make sure that thePublic endpoint
option is selected - Create the resource
Go to Azure portal and create a new Function App
resource for each Harck CMS function
List of Function Apps you will need to create
- harck-{project name}-admin
- harck-{project name}-journal
- harck-{project name}-newsletter
- harck-{project name}-private
- harck-{project name}-private-newsletter
- harck-{project name}-pub-art
- harck-{project name}-pub-cnt
- harck-{project name}-pub-files
- harck-{project name}-pub-newsletter
- harck-{project name}-signal
- Select your desired subscription. For example,
Pay-As-You-Go
- Select a resource group (It can be a good ideia to use a single resource group for all Harckade services)
- On the
Instance Details
specify any name you want for your function - Leave the
code
option selected for the deployment option - For the
Runtime stack
, select.NET
- The version should be
8
- For the region, select the one that is closer to your costumers or the one that you consider to be economically more viable
- On the
Operating system
make sure to selectLinux
- As for the hosting plan, leave the
Consumption (Serverless)
option selected and click on theNext: Storage >
button - Select thee same
Storage account
for all your harckade function apps - Go to
Monitoring
tab and select whether you want to haveApplication Insights
enabled or not. They may be useful to debug the service, but keep in mind that the may as well increase your storage costs. - Go to the
Review + create
tab and finish the creation
- Once
Function Apps
are created, open each one of them and download thepublish profile
by navigating to theOverview
window. You will need this on GitHub Actions configuration section - Then, navigate to the
configuration
section on the left side menu, clickAdvanced edit
and add the respective key-value pairs[!WARNING] Do not delete the configurations that already exist there, just add more
Configurations for each function
harck-{project name}-admin
{ "name": "AuthenticationAuthority", "value": "https://login.microsoftonline.com/{your-tenant-id}", "slotSetting": false }, { "name": "AuthenticationClientId", "value": "api://{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientId", "value": "{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientSecretValue", "value": "{your-app-registration-client-secret-value}", "slotSetting": false }, { "name": "DISPATCH_REPO", "value": "{your-github-repo>/harckade-client}", "slotSetting": false }, { "name": "GIT_TOKEN", "value": "{your-github-personal-access-token}", "slotSetting": false }, { "name": "ObjectId", "value": "{your-app-registration-object-id}", "slotSetting": false }, { "name": "RedirectUrl", "value": "{your-blog(harckade-client)-url}", "slotSetting": false }, { "name": "TenantId", "value": "{Microsoft-Entra-Id-tenant-id}", "slotSetting": false }
harck-{project name}-journal
No need to edit
harck-{project name}-newsletter
{ "name": "RedirectUrl", "value": "{your-blog(harckade-client)-url}", "slotSetting": false }, { "name": "DefaultEmailTo", "value": "{email-where-you-will-receive-notifications}", "slotSetting": false }, { "name": "EmailFrom", "value": "{email-that-your-subscribers-will-see}", "slotSetting": false }, { "name": "EmailHost", "value": "email-smtp.{regiion(e.g.:eu-west-1)}.amazonaws.com", "slotSetting": false }, { "name": "ConfigSet", "value": "", "slotSetting": false }, { "name": "SmtpPassword", "value": "{aws-ses-password}", "slotSetting": false }, { "name": "SmtpPort", "value": "587", "slotSetting": false }, { "name": "SmtpUsername", "value": "{aws-ses-smtp-username}", "slotSetting": false }
harck-{project name}-private
{ "name": "AuthenticationAuthority", "value": "https://login.microsoftonline.com/{your-tenant-id}", "slotSetting": false }, { "name": "AuthenticationClientId", "value": "api://{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientId", "value": "{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientSecretValue", "value": "{your-app-registration-client-secret-value}", "slotSetting": false }, { "name": "DISPATCH_REPO", "value": "{your-github-repo>/harckade-client}", "slotSetting": false }, { "name": "GIT_BRANCH", "value": "{branch-that-will-be-deployed-on-harckade-client}", "slotSetting": false }, { "name": "GIT_TOKEN", "value": "{your-github-personal-access-token}", "slotSetting": false }, { "name": "ObjectId", "value": "{your-app-registration-object-id}", "slotSetting": false }, { "name": "RedirectUrl", "value": "{your-blog(harckade-client)-url}", "slotSetting": false }, { "name": "TenantId", "value": "{Microsoft-Entra-Id-tenant-id}", "slotSetting": false },
harck-{project name}-private-newsletter
{ "name": "AuthenticationAuthority", "value": "https://login.microsoftonline.com/{your-tenant-id}", "slotSetting": false }, { "name": "AuthenticationClientId", "value": "api://{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientId", "value": "{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientSecretValue", "value": "{your-app-registration-client-secret-value}", "slotSetting": false }, { "name": "ObjectId", "value": "{your-app-registration-object-id}", "slotSetting": false }, { "name": "TenantId", "value": "{Microsoft-Entra-Id-tenant-id}", "slotSetting": false }, { "name": "RedirectUrl", "value": "{your-blog(harckade-client)-url}", "slotSetting": false }, { "name": "DefaultEmailTo", "value": "{email-where-you-will-receive-notifications}", "slotSetting": false }, { "name": "EmailFrom", "value": "{email-that-your-subscribers-will-see}", "slotSetting": false }, { "name": "EmailHost", "value": "email-smtp.{regiion(e.g.:eu-west-1)}.amazonaws.com", "slotSetting": false }, { "name": "ConfigSet", "value": "", "slotSetting": false }, { "name": "SmtpPassword", "value": "{aws-ses-password}", "slotSetting": false }, { "name": "SmtpPort", "value": "587", "slotSetting": false }, { "name": "SmtpUsername", "value": "{aws-ses-smtp-username}", "slotSetting": false }
harck-{project name}-pub-art
No need to edit
harck-{project name}-pub-cnt
{ "name": "DefaultEmailTo", "value": "{email-where-you-will-receive-notifications}", "slotSetting": false }, { "name": "EmailFrom", "value": "{email-that-your-subscribers-will-see}", "slotSetting": false }, { "name": "EmailHost", "value": "email-smtp.{regiion(e.g.:eu-west-1)}.amazonaws.com", "slotSetting": false }, { "name": "ConfigSet", "value": "", "slotSetting": false }, { "name": "SmtpPassword", "value": "{aws-ses-password}", "slotSetting": false }, { "name": "SmtpPort", "value": "587", "slotSetting": false }, { "name": "SmtpUsername", "value": "{aws-ses-smtp-username}", "slotSetting": false }
harck-{project name}-pub-files
No need to edit
harck-{project name}-pub-newsletter
{ "name": "DefaultEmailTo", "value": "{email-where-you-will-receive-notifications}", "slotSetting": false }, { "name": "EmailFrom", "value": "{email-that-your-subscribers-will-see}", "slotSetting": false }, { "name": "EmailHost", "value": "email-smtp.{regiion(e.g.:eu-west-1)}.amazonaws.com", "slotSetting": false }, { "name": "ConfigSet", "value": "", "slotSetting": false }, { "name": "SmtpPassword", "value": "{aws-ses-password}", "slotSetting": false }, { "name": "SmtpPort", "value": "587", "slotSetting": false }, { "name": "SmtpUsername", "value": "{aws-ses-smtp-username}", "slotSetting": false }, { "name": "RedirectUrl", "value": "https://www.harckade.com", "slotSetting": false }
harck-{project name}-signal
{ "name": "AuthenticationAuthority", "value": "https://login.microsoftonline.com/{your-tenant-id}", "slotSetting": false }, { "name": "AuthenticationClientId", "value": "api://{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientId", "value": "{your-app-registration-client-id}", "slotSetting": false }, { "name": "ClientSecretValue", "value": "{your-app-registration-client-secret-value}", "slotSetting": false }, { "name": "ObjectId", "value": "{your-app-registration-object-id}", "slotSetting": false }, { "name": "TenantId", "value": "{Microsoft-Entra-Id-tenant-id}", "slotSetting": false }, { "name": "RedirectUrl", "value": "{your-blog(harckade-client)-url}", "slotSetting": false }, { "name": "AzureSignalRConnectionString", "value": "{your-signalR-primary-Connection-String}", /* you can find it on SignalR/Keys section */ "slotSetting": false }
Create a new API Management service
resource
- Search for
API Management services
and then click onCreate
- Select your desired subscription. For example,
Pay-As-You-Go
- Give any valid name for the resource name
- For the
Region
select the region that is nearest to your users - Provide your
organization name
andadministrator email
- Select the
Consumption
tier - On the monitoring you can optionally acivate
Application Insights
but keep in mind that it will increase the running cost - On
Virtual network
make sure thatNone
option is selected for theconnectivity type
- Review and create the resource
Open your newly created API Managed service
and, under the APIs
section, navigate to the Backends
Add a Backend for the following services:
- harck-{project name}-admin
- harck-{project name}-newsletter
- harck-{project name}-private
- harck-{project name}-private-newsletter
- harck-{project name}-pub-art
- harck-{project name}-pub-cnt
- harck-{project name}-pub-files
- harck-{project name}-pub-newsletter
- harck-{project name}-signal
- Provide a valid name for each backend and for the
Type
selectAzure resource
and chose the appropriateFunction App
resource - Leave the checkboxes on
Validate certificate chain
andValidate certificate name
and hitCreate
While you are on your Harckade's API Managed service
, navigate to APIs
and add a new API
- Click on the
Add API
and select HTTP option - Provide a valid
display name
andname
and then clickcreate
- Select your newly created API and on the
Frontend
open theOpenAPI specification editor
by clicking on the pencil icon - Copy the
url
fromservers
section and save it somewhere as note (you can remove it after the next step) - Select all text and replace it with the following code
OpenAPI specification JSON (replace the line number 9 with your own URL that you copied on the previous step):
{ "openapi": "3.0.1", "info": { "title": "Harckade Backend", "description": "", "version": "1.0" }, "servers": [{ "url": "{YOUR_SERVER_URL}" }], "paths": { "/files/{*path}": { "get": { "summary": "DownloadFile", "description": "Download files using public API (without authentication)", "operationId": "downloadfile", "parameters": [{ "name": "*path", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/cms/users": { "get": { "summary": "ListUsers", "description": "ListUsers", "operationId": "get-listusers", "responses": { "200": { "description": "null" } } }, "post": { "summary": "InviteUser", "description": "InviteUser", "operationId": "post-inviteuser", "responses": { "200": { "description": "null" } } }, "patch": { "summary": "EditUser", "description": "EditUser", "operationId": "patch-edituser", "responses": { "200": { "description": "null" } } } }, "/cms/settings": { "post": { "summary": "UpdateSettings", "description": "UpdateSettings", "operationId": "post-updatesettings", "responses": { "200": { "description": "null" } } }, "get": { "summary": "GetSettings", "description": "GetSettings", "operationId": "get-getsettings", "responses": { "200": { "description": "null" } } } }, "/cms/journal": { "get": { "summary": "GetJournal", "description": "GetJournal", "operationId": "get-getjournal", "responses": { "200": { "description": "null" } } } }, "/cms/users/{userId}": { "delete": { "summary": "DeleteUser", "description": "DeleteUser", "operationId": "delete-deleteuser", "parameters": [{ "name": "userId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/languages": { "get": { "summary": "GetLanguages", "description": "GetLanguages", "operationId": "get-getlanguages", "responses": { "200": { "description": "null" } } } }, "/languages/default": { "get": { "summary": "GetDefaultLanguage", "description": "GetDefaultLanguage", "operationId": "get-getdefaultlanguage", "responses": { "200": { "description": "null" } } } }, "/cms/zip-files": { "post": { "summary": "ZipFiles", "description": "ZipFiles", "operationId": "post-zipfiles", "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}/recover": { "patch": { "summary": "RecoverDeletedArticleById", "description": "RecoverDeletedArticleById", "operationId": "patch-recoverdeletedarticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/files/{*path}": { "get": { "summary": "ListFiles", "description": "ListFiles", "operationId": "get-listfiles", "parameters": [{ "name": "*path", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } }, "post": { "summary": "UploadFile", "description": "UploadFile", "operationId": "post-uploadfile", "parameters": [{ "name": "*path", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } }, "delete": { "summary": "DeleteFile", "description": "DeleteFile", "operationId": "delete-deletefile", "parameters": [{ "name": "*path", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}/permanent": { "delete": { "summary": "PermanentlyDeleteArticleById", "description": "PermanentlyDeleteArticleById", "operationId": "delete-permanentlydeletearticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/articles/deleted": { "get": { "summary": "ListAllDeletedArticles", "description": "ListAllDeletedArticles", "operationId": "get-listalldeletedarticles", "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}": { "patch": { "summary": "PublishArticleById", "description": "PublishArticleById", "operationId": "patch-publisharticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } }, "get": { "summary": "GetArticleById", "description": "GetArticleById", "operationId": "get-getarticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } }, "delete": { "summary": "DeleteArticleById", "description": "DeleteArticleById", "operationId": "delete-deletearticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}/{lang}/history/{timestamp}": { "patch": { "summary": "RestoreArticleToBackup", "description": "RestoreArticleToBackup", "operationId": "patch-restorearticletobackup", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "lang", "in": "path", "required": true, "schema": { "type": "" } }, { "name": "timestamp", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } }, "get": { "summary": "GetBackupArticleContentById", "description": "GetBackupArticleContentById", "operationId": "get-getbackuparticlecontentbyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "lang", "in": "path", "required": true, "schema": { "type": "" } }, { "name": "timestamp", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/robots.txt": { "get": { "summary": "RobotsTxt", "description": "RobotsTxt", "operationId": "get-robotstxt", "responses": { "200": { "description": "null" } } } }, "/cms/articles": { "get": { "summary": "ListAllArticles", "description": "ListAllArticles", "operationId": "get-listallarticles", "responses": { "200": { "description": "null" } } }, "put": { "summary": "AddUpdateArticle", "description": "AddUpdateArticle", "operationId": "put-addupdatearticle", "responses": { "200": { "description": "null" } } } }, "/cms/deploy": { "get": { "summary": "LaunchDeployment", "description": "LaunchDeployment", "operationId": "get-launchdeployment", "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}/content": { "get": { "summary": "GetArticleContentById", "description": "GetArticleContentById", "operationId": "get-getarticlecontentbyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/articles/{articleId}/{lang}/history": { "get": { "summary": "GetArticleHistory", "description": "GetArticleHistory", "operationId": "get-getarticlehistory", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }, { "name": "lang", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/cms/files": { "put": { "summary": "AddFolder", "description": "AddFolder", "operationId": "put-addfolder", "responses": { "200": { "description": "null" } } }, "get": { "summary": "ListFilesRoot", "description": "ListFilesRoot", "operationId": "get-listfiles-root", "responses": { "200": { "description": "null" } } }, "post": { "summary": "UploadFileRoot", "description": "UploadFileRoot", "operationId": "post-uploadfile-root", "responses": { "200": { "description": "null" } } } }, "/articles/title/{lang}/{title}": { "get": { "summary": "GetPublishedArticleByTitle", "description": "GetPublishedArticleByTitle", "operationId": "get-getpublishedarticlebytitle", "parameters": [{ "name": "lang", "in": "path", "required": true, "schema": { "type": "" } }, { "name": "title", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/articles": { "get": { "summary": "ListArticles", "description": "ListArticles", "operationId": "get-listarticles", "responses": { "200": { "description": "null" } } } }, "/articles/{articleId}/content": { "get": { "summary": "GetPublishedArticleContentById", "description": "GetPublishedArticleContentById", "operationId": "get-getpublishedarticlecontentbyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/articles/title/{lang}/{title}/content": { "get": { "summary": "GetPublishedArticleContentByTitle", "description": "GetPublishedArticleContentByTitle", "operationId": "get-getpublishedarticlecontentbytitle", "parameters": [{ "name": "lang", "in": "path", "required": true, "schema": { "type": "" } }, { "name": "title", "in": "path", "required": true, "schema": { "type": "" } }], "responses": { "200": { "description": "null" } } } }, "/articles/{articleId}": { "get": { "summary": "GetPublishedArticleById", "description": "GetPublishedArticleById", "operationId": "get-getpublishedarticlebyid", "parameters": [{ "name": "articleId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/contact": { "post": { "summary": "SendContactForm", "description": "SendContactForm", "operationId": "post-sendcontactform", "responses": { "200": { "description": "null" } } } }, "/newsletter/unsubscribe": { "post": { "summary": "UnsubscribeNewsletter", "description": "UnsubscribeNewsletter", "operationId": "post-unsubscribenewsletter", "responses": { "200": { "description": "null" } } } }, "/newsletter": { "post": { "summary": "SubscribeToNewsletter", "description": "SubscribeToNewsletter", "operationId": "post-subscribetonewsletter", "responses": { "200": { "description": "null" } } } }, "/newsletter/confirm": { "post": { "summary": "ConfirmNewsletterEmail", "description": "ConfirmNewsletterEmail", "operationId": "post-confirmnewsletteremail", "responses": { "200": { "description": "null" } } } }, "/cms/newsletters/{newsletterId}/send": { "get": { "summary": "SendNewsletterToQueue", "description": "SendNewsletterToQueue", "operationId": "get-sendnewslettertoqueue", "parameters": [{ "name": "newsletterId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/subscribers/{subscriberId}": { "delete": { "summary": "RemoveSubscriber", "description": "RemoveSubscriber", "operationId": "delete-removesubscriber", "parameters": [{ "name": "subscriberId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/newsletters/{newsletterId}": { "get": { "summary": "GetNewsletterById", "description": "GetNewsletterById", "operationId": "get-getnewsletterbyid", "parameters": [{ "name": "newsletterId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } }, "delete": { "summary": "DeleteNewsletterById", "description": "DeleteNewsletterById", "operationId": "delete-deletenewsletterbyid", "parameters": [{ "name": "newsletterId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/subscribers": { "get": { "summary": "ListAllSubscribers", "description": "ListAllSubscribers", "operationId": "get-listallsubscribers", "responses": { "200": { "description": "null" } } } }, "/cms/newsletters": { "get": { "summary": "ListAllNewsletters", "description": "ListAllNewsletters", "operationId": "get-listallnewsletters", "responses": { "200": { "description": "null" } } }, "put": { "summary": "AddUpdateNewsletter", "description": "AddUpdateNewsletter", "operationId": "put-addupdatenewsletter", "responses": { "200": { "description": "null" } } } }, "/cms/newsletters/{newsletterId}/content": { "get": { "summary": "GetNewsletterContentById", "description": "GetNewsletterContentById", "operationId": "get-getnewslettercontentbyid", "parameters": [{ "name": "newsletterId", "in": "path", "required": true, "schema": { "type": "string" } }], "responses": { "200": { "description": "null" } } } }, "/cms/subscription-template": { "get": { "summary": "GetNewsletterSubscriptionTemplate", "description": "GetNewsletterSubscriptionTemplate", "operationId": "get-getnewslettersubscriptiontemplate", "responses": { "200": { "description": "null" } } }, "put": { "summary": "AddOrUpdateNewsletterSubscriptionTemplate", "description": "AddOrUpdateNewsletterSubscriptionTemplate", "operationId": "put-addorupdatenewslettersubscriptiontemplate", "responses": { "200": { "description": "null" } } } }, "/cms/subscription-template/content": { "get": { "summary": "GetNewsletterContent", "description": "GetNewsletterContent", "operationId": "get-getnewslettercontent", "responses": { "200": { "description": "null" } } } }, "/cms/notifications/negotiate": { "get": { "summary": "signalRNegotiate", "description": "signalRNegotiate", "operationId": "get-signalrnegotiate", "responses": { "200": { "description": "null" } } }, "post": { "summary": "signalRNegotiate", "description": "signalRNegotiate", "operationId": "post-signalrnegotiate", "responses": { "200": { "description": "null" } } } }, "/cms/notifications/sendMessage": { "post": { "summary": "signalRSendMessage", "description": "signalRSendMessage", "operationId": "post-signalrsendmessage", "responses": { "200": { "description": "null" } } } } }, "components": { "securitySchemes": { "apiKeyHeader": { "type": "apiKey", "name": "Ocp-Apim-Subscription-Key", "in": "header" }, "apiKeyQuery": { "type": "apiKey", "name": "subscription-key", "in": "query" } } }, "security": [{ "apiKeyHeader": [] }, { "apiKeyQuery": [] }] }
- Add Inbound and outbout processing rules by selecting the API, and then click on the
Policy code editor
which should be located on the<>
icon under Inbound/Outband processingReplace {YOUR_FRONTEND_URL} with your own frontend URL. Optionally, you can add multiple entires (E.g.:http://localhost:3000). Also, do not forget to update {project name}:
<policies> <inbound> <base /> <choose> <when condition="@(context.Request.Url.Path.StartsWith("/files") || context.Request.Url.Path.StartsWith("files"))"> <set-backend-service backend-id="harck-{project name}-pub-files" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/cms/users") || context.Request.Url.Path.StartsWith("/cms/settings") || context.Request.Url.Path.StartsWith("/cms/journal") || context.Request.Url.Path.StartsWith("/languages") || context.Request.Url.Path.StartsWith("cms/users") || context.Request.Url.Path.StartsWith("cms/settings") || context.Request.Url.Path.StartsWith("cms/journal") || context.Request.Url.Path.StartsWith("languages"))"> <set-backend-service backend-id="harck-{project name}-admin" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/cms/zip-files") || context.Request.Url.Path.StartsWith("/cms/articles") || context.Request.Url.Path.StartsWith("/cms/files") || context.Request.Url.Path.StartsWith("/robots") || context.Request.Url.Path.StartsWith("/cms/deploy") || context.Request.Url.Path.StartsWith("cms/zip-files") || context.Request.Url.Path.StartsWith("cms/articles") || context.Request.Url.Path.StartsWith("cms/files") || context.Request.Url.Path.StartsWith("robots") || context.Request.Url.Path.StartsWith("cms/deploy"))"> <set-backend-service backend-id="harck-{project name}-private" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/articles") || context.Request.Url.Path.StartsWith("articles"))"> <set-backend-service backend-id="harck-{project name}-pub-art" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/contact") || context.Request.Url.Path.StartsWith("contact"))"> <set-backend-service backend-id="harck-{project name}-pub-cnt" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/newsletter") || context.Request.Url.Path.StartsWith("newsletter"))"> <set-backend-service backend-id="harck-{project name}-pub-newsletter" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/cms/newsletter") || context.Request.Url.Path.StartsWith("/cms/subscribers") || context.Request.Url.Path.StartsWith("/cms/subscription-template") || context.Request.Url.Path.StartsWith("cms/newsletter") || context.Request.Url.Path.StartsWith("cms/subscribers") || context.Request.Url.Path.StartsWith("cms/subscription-template"))"> <set-backend-service backend-id="harck-{project name}-private-newsletter" /> </when> <when condition="@(context.Request.Url.Path.StartsWith("/cms/notifications") || context.Request.Url.Path.StartsWith("cms/notifications"))"> <set-backend-service backend-id="harck-{project name}-signal" /> </when> <!-- default condition --> <otherwise> <return-response> <set-status code="404" reason="Not Found" /> </return-response> </otherwise> </choose> <cors allow-credentials="true"> <allowed-origins> <origin>https://{YOUR_FRONTEND_URL}.azurestaticapps.net</origin> </allowed-origins> <allowed-methods> <method>GET</method> <method>POST</method> <method>OPTIONS</method> <method>PUT</method> <method>PATCH</method> <method>DELETE</method> </allowed-methods> <allowed-headers> <header>*</header> </allowed-headers> <expose-headers> <header>*</header> </expose-headers> </cors> </inbound> <backend> <base /> </backend> <outbound> <base /> <set-header name="Cache-Control" exists-action="override"> <value>@{ return context.Response.Headers.GetValueOrDefault("Cache-Control", ""); }</value> </set-header> <set-header name="Date" exists-action="override"> <value>@{ return context.Response.Headers.GetValueOrDefault("Date", ""); }</value> </set-header> <set-header name="Expires" exists-action="override"> <value>@{ return context.Response.Headers.GetValueOrDefault("Expires", ""); }</value> </set-header> <set-header name="Request-Context" exists-action="override"> <value>@{ return context.Response.Headers.GetValueOrDefault("Request-Context", ""); }</value> </set-header> <set-header name="Server" exists-action="override"> <value>@{ return "Harckade"; }</value> </set-header> <set-header name="Content-Type" exists-action="override"> <value>@{ return context.Response.Headers.GetValueOrDefault("Content-Type", ""); }</value> </set-header> <!-- Retry Policy --> <retry condition="@(context.Response.StatusCode == 503 || context.Response.StatusCode == 500)" count="3" interval="10" max-interval="30" delta="1" first-fast-retry="true"> <set-header name="Retry-After" exists-action="override"> <value>10</value> </set-header> </retry> </outbound> <on-error> <base /> </on-error> </policies>
To setup a custom domain, navigate to the Custom domains
tab and add your own domain
For this configuration you will need the Publishing profiles
from the Function App configuration - step 1
. Once you have them, open the repository you have cloned and navigate to Settings
section.
There, on the left side expand the Secrets and variables
under Security
section and click on Actions
.
You need to configure the following secrets (make sure the secrets names are spelled correctly, as they are used by GitHub Actions workflow):
- PUBLISH_ADMIN
- PUBLISH_ARTICLES
- PUBLISH_CONTACT
- PUBLISH_FILES
- PUBLISH_JOURNAL
- PUBLISH_NEWSLETTER
- PUBLISH_PRIVATE
- PUBLISH_PRIVATE_NEWSLETTER
- PUBLISH_PUBLIC_NEWSLETTER
- PUBLISH_SIGNALR
Congratulations! You backend should be fully operational!
If you want to debug this project locally you can use any C# and .NET compatible IDE. On this guide, the focus will be on the Microsoft Visual Studio.
- Microsoft Visual Studio
- Azurite
Load the project, and add the following file for the function that you want to debug:
local.settings.json
{ "IsEncrypted": false, "Values": { "AzureWebJobsStorage": "{storage-connection-string}", "FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated", "AuthenticationAuthority": "https://login.microsoftonline.com/{your-Microsoft-Entra-Id-tenant-id}", "AuthenticationClientId": "api://{your-app-registration-client-id}", "ClientId": "{your-app-registration-client-id}", "ObjectId": "{your-app-registration-object-id}", "TenantId": "{your-Microsoft-Entra-Id-tenant-id}", "ClientSecretValue": "{your-app-registration-client-secret-value}", "RedirectUrl": "http://localhost:3000", "DISPATCH_REPO": "{your-github-repo>/harckade-client}", "GIT_TOKEN": "{your-github-personal-access-token}", "GIT_BRANCH": "{branch-that-will-be-deployed-on-harckade-client}", "SmtpPassword": "{aws-ses-password}", "SmtpPort": "587", "SmtpUsername": "{aws-ses-smtp-username}", "EmailFrom": "{email-that-your-subscribers-will-see}", "EmailHost": "email-smtp.{regiion(e.g.:eu-west-1)}.amazonaws.com" }, "Host": { "LocalHttpPort": 7071, "CORS": "http://localhost:3000", "CORSCredentials": true } }
Note
If you want to run multiple functions simulatenously make sure that the LocalHttpPort
value is unique for each function. For example (7071, 7072, 7073, etc.).
Then select the Function you want to test, for example Harckade.CMS.PublicController.Files
, click on it with the right mouse button, navigate to the Debug
option and click on the Start new Instance