{"id":37474557,"url":"https://github.com/grantcolley/atlas","last_synced_at":"2026-01-16T07:20:01.475Z","repository":{"id":191262849,"uuid":"683814017","full_name":"grantcolley/atlas","owner":"grantcolley","description":"A .NET 9.0 Blazor framework for hosting and building Blazor applications using the Backend for Frontend (BFF) pattern using: Blazor, ASP.NET Core Web API, Auth0, FluentUI, FluentValidation, Backend for Frontend (BFF), Entity Framework Core, MS SQL Server, SQLite","archived":false,"fork":false,"pushed_at":"2025-03-29T19:45:35.000Z","size":7221,"stargazers_count":9,"open_issues_count":2,"forks_count":1,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-03-29T20:28:44.743Z","etag":null,"topics":["auth0","backendforfrontend","blazor","blazor-client","blazor-server","fluent-ui","fluentui","fluentvalidation","minimal-api","net9","net90","oauth2","sqlite","sqlite3","sqlserver","wasm","webapi","webassembly"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/grantcolley.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-08-27T19:26:18.000Z","updated_at":"2025-03-14T18:10:48.000Z","dependencies_parsed_at":"2023-09-29T22:36:26.117Z","dependency_job_id":"75f18c95-1b43-4a0b-9815-18ca754bf64f","html_url":"https://github.com/grantcolley/atlas","commit_stats":null,"previous_names":["grantcolley/atlas"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/grantcolley/atlas","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grantcolley%2Fatlas","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grantcolley%2Fatlas/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grantcolley%2Fatlas/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grantcolley%2Fatlas/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/grantcolley","download_url":"https://codeload.github.com/grantcolley/atlas/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/grantcolley%2Fatlas/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28478047,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-16T06:30:42.265Z","status":"ssl_error","status_checked_at":"2026-01-16T06:30:16.248Z","response_time":107,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["auth0","backendforfrontend","blazor","blazor-client","blazor-server","fluent-ui","fluentui","fluentvalidation","minimal-api","net9","net90","oauth2","sqlite","sqlite3","sqlserver","wasm","webapi","webassembly"],"created_at":"2026-01-16T07:20:01.319Z","updated_at":"2026-01-16T07:20:01.464Z","avatar_url":"https://github.com/grantcolley.png","language":"C#","readme":"![Alt text](/readme-images/Atlas.png?raw=true \"Atlas\") \n\n###### .NET 9.0, Blazor, ASP.NET Core Web API, Azure, Auth0, FluentUI, FluentValidation, Backend for Frontend (BFF), Entity Framework Core, MS SQL Server, SQLite\n\nA .NET 9.0 Blazor framework for hosting and building Blazor applications using the Backend for Frontend (BFF) pattern. It comes with authentication, authorisation, change tracking, and persisting structured logs to the database. Follow the instructions for publishing to Azure.\n\nSee the [Worked Examples](#worked-examples) for step-by-step guidance on how to introduce new modules into the **Atlas** framework.\n\n\\\n[![Build status](https://ci.appveyor.com/api/projects/status/qx6pbauk9bfpopst?svg=true)](https://ci.appveyor.com/project/grantcolley/atlas)\n\n\\\n![Alt text](/readme-images/Atlas_Architecture.png?raw=true \"Atlas Architecture\")\n\n## Table of Contents\n* [Setup the Solution](#setup-the-solution)\n    * [Multiple Startup Projects](#multiple-startup-projects)\n    * [Atlas.API Configuration](#atlasapi-configuration)\n    * [Atlas.Blazor.Web.App Configuration](#atlasblazorwebapp-configuration)\n    * [Create the Database](#create-the-database)\n    * [Insert Seed Data](#insert-seed-data)\n* [Authentication](#authentication)\n    * [Create an Auth0 Role](#create-an-auth0-role)\n    * [Create Auth0 Users](#create-auth0-users)\n    * [Securing Atlas.API](#securing-atlasapi)\n    * [Securing Atlas.Blazor.Web.App](#securing-atlasblazorwebapp)\n    * [Log In](#log-in)\n* [Authorization](#authorization)\n    * [Users, Roles and Permissions](#users-roles-and-permissions)\n* [Support Role](#support-role)\n    * [Logging](#logging)\n* [Navigation](#navigation)\n    * [Modules, Categories and Pages](#modules-categories-and-pages)\n* [Audit](#audit)\n* [Publish Atlas to Azure](#publish-atlas-to-azure)\n  * [Steps to Publish Atlas to Azure](#steps-to-publish-atlas-to-azure)\n    * [Resource Group](#resource-group)   \n    * [Web App + Database](#web-app--database)\n    * [Web App](#web-app)\n    * [Environment Variables](#environment-variables)\n    * [Auth0](#auth0)\n    * [Update Web API Configuration and Publish to Azure](#update-web-api-configuration-and-publish-to-azure)\n    * [Update Blazor Web App Configuration and Publish to Azure](#update-blazor-web-app-configuration-and-publish-to-azure)\n* [Worked Examples](#worked-examples)\n    * [Blazor Template](#blazor-template) \n* [Notes](#notes)\n    * [FluentDesignTheme Dark/Light](#fluentdesigntheme-darklight)\n    * [Backend for frontend](#backend-for-frontend)\n\n# Setup the Solution\n\n### Multiple Startup Projects\nIn the _Solution Properties_, specify multiple startup projects and set the action for both **Atlas.API** Web API and **Atlas.Blazor.Web.App** Blazor application, to *Start*.\n\n![Alt text](/readme-images/Solution_Property_Pages.png?raw=true \"Solution Properties\") \n\n### Atlas.API Configuration\nIn the **Atlas.API** [appsettings.json](https://github.com/grantcolley/atlas/blob/main/src/Atlas.API/appsettings.json) set the connection strings, configure [Auth0](https://auth0.com/) settings and generating seed data.\n\n\u003e [!NOTE]  \n\u003e Read the next section on [Authentication](#authentication) for how to configure [Auth0](https://auth0.com/) as the identity provider. \n\n```json\n{\n  \"ConnectionStrings\": {\n    \"DefaultConnection\": \"\"   👈 set the Atlas database connection string\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Microsoft.EntityFrameworkCore.Database.Command\": \"Information\"\n    }\n  },\n  \"Serilog\": {\n    \"Using\": [ \"Serilog.Sinks.MSSqlServer\" ],\n    \"MinimumLevel\": {\n      \"Default\": \"Information\",\n      \"Override\": {\n        \"Microsoft\": \"Error\",\n        \"Microsoft.EntityFrameworkCore.Database.Command\": \"Information\"\n      }\n    },\n    \"WriteTo\": [\n      {\n        \"Name\": \"MSSqlServer\",\n        \"Args\": {\n          \"connectionString\": \"\",   👈set the Atlas database connection string for Serilogs MS SqlServer\n          \"tableName\": \"Logs\",\n          \"autoCreateSqlTable\": true,\n          \"columnOptionsSection\": {\n            \"customColumns\": [\n              {\n                \"ColumnName\": \"User\",\n                \"DataType\": \"nvarchar\",\n                \"DataLength\": 450\n              },\n              {\n                \"ColumnName\": \"Context\",\n                \"DataType\": \"nvarchar\",\n                \"DataLength\": 450\n              }\n            ]\n          }\n        }\n      }\n    ]\n  },\n  \"AllowedHosts\": \"*\",\n  \"Auth0\": {\n    \"Domain\": \"\",                        👈specify the Auth0 domain\n    \"Audience\": \"https://Atlas.API.com\"  👈specify the audience\n  },\n  \"SeedData\": {\n    \"GenerateSeedData\": \"true\", 👈 set to true to create seed data including modules, categories, pages, users, permissions and roles.\n    \"GenerateSeedLogs\":  \"true\" 👈 set to true to generate mock logs\n  }\n}\n```\n\n### Atlas.Blazor.Web.App Configuration\nIn the **Atlas.Blazor.Web.App** [appsettings.json](https://github.com/grantcolley/atlas/blob/main/src/Atlas.Blazor.Web.App/appsettings.json) configure [Auth0](https://auth0.com/) settings and specify the **Atlas.API** url.\n\n\u003e [!NOTE]  \n\u003e Read the next section on [Authentication](#authentication) for how to configure [Auth0](https://auth0.com/) as the identity provider.\n\n```json\n{\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Microsoft.EntityFrameworkCore.Database.Command\": \"Warning\"\n    }\n  },\n  \"Serilog\": {\n    \"Using\": [ \"Serilog.Sinks.MSSqlServer\" ],\n    \"MinimumLevel\": {\n      \"Default\": \"Information\",\n      \"Override\": {\n        \"Microsoft\": \"Error\",\n        \"Microsoft.EntityFrameworkCore.Database.Command\": \"Information\"\n      }\n    },\n    \"WriteTo\": [\n      {\n        \"Name\": \"MSSqlServer\",\n        \"Args\": {\n          \"connectionString\": \"\",    👈set the Atlas database connection string for Serilogs MS SqlServer\n          \"tableName\": \"Logs\",\n          \"autoCreateSqlTable\": true,\n          \"columnOptionsSection\": {\n            \"customColumns\": [\n              {\n                \"ColumnName\": \"User\",\n                \"DataType\": \"nvarchar\",\n                \"DataLength\": 450\n              },\n              {\n                \"ColumnName\": \"Context\",\n                \"DataType\": \"nvarchar\",\n                \"DataLength\": 450\n              }\n            ]\n          }\n        }\n      }\n    ]\n  },\n  \"AllowedHosts\": \"*\",\n  \"Auth0\": {\n    \"Domain\": \"\",                           👈specify the Auth0 domain\n    \"ClientId\": \"\",                         👈specify the Auth0 ClientId\n    \"ClientSecret\": \"\",                     👈specify the Auth0 ClientSecret\n    \"Audience\": \"https://Atlas.API.com\"     👈specify the audience\n  },\n  \"AtlasAPI\": \"https://localhost:44420\"     👈specify the AtlasAPI url\n}\n```\n\n### Create the Database\nUse the `.NET CLI` for Entity Framework to create your database and create your schema from the migration. In the `Developer Powershell` or similar, navigate to the **Atlas.API** folder and run the following command.\n\n```\ndotnet ef database update --project ..\\..\\data\\Atlas.Migrations.SQLServer\n```\n\n### Insert Seed Data\nIn the **Atlas.API** [appsettings.json](https://github.com/grantcolley/atlas/blob/f00c8113c0c79c44718d347a139ab877e63a7b88/src/Atlas.API/appsettings.json#L51-L53) configuration file set `GenerateSeedData` and `GenerateSeedLogs` to true. This will populate the database with seed data at startup.\n\n\u003e [!WARNING]  \n\u003e If `\"GenerateSeedData\": \"true\"` the tables in the Atlas database will be truncated and repopulated with seed data every time the application starts. Existing data will be permanently lost.\n\n```json\n  \"SeedData\": {\n    \"GenerateSeedData\": \"true\", 👈 set to true to create seed data including modules, categories, pages, users, permissions and roles.\n    \"GenerateSeedLogs\":  \"true\" 👈 set to true to generate mock logs\n  }\n```\n\n# Authentication\nAtlas is setup to use [Auth0](https://auth0.com/) as its authentication provider, although this can be swapped out for any provider supporting **OAuth 2.0**. With [Auth0](https://auth0.com/signup?place=header\u0026type=button\u0026text=sign%20up) you can create a free account and it has a easy to use dashboard for registering applications, and creating and managing roles and users.\n\nUsing the [Auth0](https://auth0.com/), register the **Atlas.API** Web API and **Atlas.Blazor.Web.App** Blazor application, and  create a `atlas-user` role and users.\n\n### Create an Auth0 Role\nIn the [Auth0](https://auth0.com/) dashboard create a role called `atlas-user`. This role must be assigned to all users wishing to access the Atlas application.\n\n\u003e [!IMPORTANT]  \n\u003e Atlas users must be assigned the `atlas-user` role in [Auth0](https://auth0.com/) to access the Atlas application.\n\n![Alt text](/readme-images/Auth0_Role.png?raw=true \"Auth0 Role\") \n\n### Create Auth0 Users\nCreate Auth0 users. The user's [Auth0](https://auth0.com/) email claim is mapped to the email of an authorised user in the Atlas database.\n\n\u003e [!IMPORTANT]  \n\u003e Atlas users must be assigned the `atlas-user` role to access the Atlas application.\n\n![Alt text](/readme-images/Auth0_User.png?raw=true \"Auth0 User\") \n\n![Alt text](/readme-images/Auth0_User_Role.png?raw=true \"Auth0 User Role\") \n\n\u003e [!TIP]\n\u003e [SeedData.cs](https://github.com/grantcolley/atlas/blob/9509c67c874711e1760bbf5cd6561c662abe2e81/data/Atlas.Seed.Data/SeedData.cs#L111-L114) already contains some pre-defined sample users with roles and permissions. Either create these users in [Auth0](https://auth0.com/), or amend the sample users in [SeedData.cs](https://github.com/grantcolley/atlas/blob/9509c67c874711e1760bbf5cd6561c662abe2e81/data/Atlas.Seed.Data/SeedData.cs#L111-L114) to reflect those created in [Auth0](https://auth0.com/).\n\n\u003e [!WARNING]  \n\u003e If `\"GenerateSeedData\": \"true\"` the tables in the Atlas database will be truncated and repopulated with seed data every time the application starts. Existing data will be permanently lost.\n\n```C#\n        private static void CreateUsers()\n        {\n            if (dbContext == null) throw new NullReferenceException(nameof(dbContext));\n\n            users.Add(\"alice\", new User { Name = \"alice\", Email = \"alice@email.com\" });\n            users.Add(\"jane\", new User { Name = \"jane\", Email = \"jane@email.com\" });\n            users.Add(\"bob\", new User { Name = \"bob\", Email = \"bob@email.com\" });\n            users.Add(\"grant\", new User { Name = \"grant\", Email = \"grant@email.com\" });\n\n            foreach (User user in users.Values)\n            {\n                dbContext.Users.Add(user);\n            }\n\n            dbContext.SaveChanges();\n        }\n```\n\n\n\n![Alt text](/readme-images/Auth0_User.png?raw=true \"Auth0 Users\") \n\n### Securing Atlas.API\nThe following article explains how to register and [secure a minimal WebAPI with Auth0](https://auth0.com/blog/securing-aspnet-minimal-webapis-with-auth0/) with the relevant parts in the **Atlas.API** [Program.cs](https://github.com/grantcolley/atlas/blob/main/src/Atlas.API/Program.cs).\n\n```C#\n\n//....existing code removed for brevity\n\nbuilder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)\n    .AddJwtBearer(options =\u003e\n    {\n        options.Authority = $\"https://{builder.Configuration[\"Auth0:Domain\"]}\";\n        options.TokenValidationParameters = new TokenValidationParameters\n        {\n            ValidIssuer = builder.Configuration[\"Auth0:Domain\"],\n            ValidAudience = builder.Configuration[\"Auth0:Audience\"]\n        };\n    });\n\nbuilder.Services.AddAuthorizationBuilder()\n    .AddPolicy(Auth.ATLAS_USER_CLAIM, policy =\u003e\n    {\n        policy.RequireAuthenticatedUser().RequireRole(Auth.ATLAS_USER_CLAIM);\n    });\n\n//....existing code removed for brevity\n\napp.UseAuthentication();\n\napp.UseAuthorization();\n\n//....existing code removed for brevity\n\n```\n\nWhen mapping the minimal Web API methods add `RequireAuthorization(Auth.ATLAS_USER_CLAIM)`, as can be seen here in [AtlasEndpointMapper.cs](https://github.com/grantcolley/atlas/blob/main/src/Atlas.API/Extensions/AtlasEndpointMapper.cs).\n\n```C#\n\n//....existing code removed for brevity\n\napp.MapGet($\"/{AtlasAPIEndpoints.GET_CLAIM_MODULES}\", ClaimEndpoint.GetClaimModules)\n            .WithOpenApi()\n            .WithName(AtlasAPIEndpoints.GET_CLAIM_MODULES)\n            .WithDescription(\"Gets the user's authorized modules\")\n            .Produces\u003cIEnumerable\u003cModule\u003e?\u003e(StatusCodes.Status200OK)\n            .Produces(StatusCodes.Status500InternalServerError)\n            .RequireAuthorization(Auth.ATLAS_USER_CLAIM);  // 👈 add RequireAuthorization to endpoints\n\n//....existing code removed for brevity\n\n```\n\n### Securing Atlas.Blazor.Web.App\nThe following article explains how to register and [add Auth0 Authentication to Blazor Web Apps](https://auth0.com/blog/auth0-authentication-blazor-web-apps/) in .NET 8.0.\n\n\u003e [!WARNING]  \n\u003e .NET 9.0 simplifies this approach by providing services to serialize the authentication state on the server and deserialize the authentication state in the WebAssembly client.\n\u003e\n\u003e See this article for details explaining [Authentication State Serialization for Blazor Web Apps](https://auth0.com/blog/authentication-authorization-enhancements-in-dotnet-9/#Authentication-State-Serialization-for-Blazor-Web-Apps).\n\u003e \n\u003e See Microsoft's documentation to [Manage authentication state in Blazor Web Apps](https://learn.microsoft.com/en-us/aspnet/core/blazor/security/?view=aspnetcore-9.0\u0026tabs=visual-studio#manage-authentication-state-in-blazor-web-apps).\n\nHere are the relevant parts in the **Atlas.Blazor.Web.App** [Program.cs](https://github.com/grantcolley/atlas-origin/blob/main/src/Atlas.Blazor.Web.App/Program.cs).\n\n```C#\n\n//....existing code removed for brevity\n\nbuilder.Services.AddRazorComponents()\n    .AddInteractiveServerComponents()\n    .AddInteractiveWebAssemblyComponents()\n    .AddAuthenticationStateSerialization();  // 👈 adds the services to serialize the authentication state on the server\n\n//....existing code removed for brevity\n\nbuilder.Services\n    .AddAuth0WebAppAuthentication(Auth0Constants.AuthenticationScheme, options =\u003e\n    {\n        options.Domain = builder.Configuration[\"Auth0:Domain\"] ?? throw new NullReferenceException(\"Auth0:Domain\");\n        options.ClientId = builder.Configuration[\"Auth0:ClientId\"] ?? throw new NullReferenceException(\"Auth0:ClientId\");\n        options.ClientSecret = builder.Configuration[\"Auth0:ClientSecret\"] ?? throw new NullReferenceException(\"Auth0:ClientSecret\");\n        options.ResponseType = \"code\";\n    }).WithAccessToken(options =\u003e\n    {\n        options.Audience = builder.Configuration[\"Auth0:Audience\"] ?? throw new NullReferenceException(\"Auth0:Audience\");\n    });\n\nbuilder.Services.AddCascadingAuthenticationState();\nbuilder.Services.AddHttpContextAccessor();\nbuilder.Services.AddScoped\u003cTokenHandler\u003e();\n\n//....existing code removed for brevity\n\napp.MapGet(\"login\", async (HttpContext httpContext, string redirectUri = @\"/\") =\u003e\n{\n    AuthenticationProperties authenticationProperties = new LoginAuthenticationPropertiesBuilder()\n            .WithRedirectUri(redirectUri)\n            .Build();\n\n    await httpContext.ChallengeAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);\n});\n\napp.MapGet(\"logout\", async (HttpContext httpContext, string redirectUri = @\"/\") =\u003e\n{\n    AuthenticationProperties authenticationProperties = new LogoutAuthenticationPropertiesBuilder()\n            .WithRedirectUri(redirectUri)\n            .Build();\n\n    await httpContext.SignOutAsync(Auth0Constants.AuthenticationScheme, authenticationProperties);\n    await httpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);\n});\n\n//....existing code removed for brevity\n\n```\n\nHere is the relevant part in the **Atlas.Blazor.Web.Client** [Program.cs](https://github.com/grantcolley/atlas-origin/blob/main/src/Atlas.Blazor.Web.App/Program.cs).\n\n```C#\nWebAssemblyHostBuilder builder = WebAssemblyHostBuilder.CreateDefault(args);\n\nbuilder.Services.AddFluentUIComponents();\n\nbuilder.Services.AddAuthorizationCore();\nbuilder.Services.AddCascadingAuthenticationState();\nbuilder.Services.AddAuthenticationStateDeserialization(); // 👈 adds the services to deserialize the authentication state in the WebAssembly client\n\nawait builder.Build().RunAsync();\n```\n\nFinally, the following article describes how to [call protected APIs from a Blazor Web App](https://auth0.com/blog/call-protected-api-from-blazor-web-app/), including [calling external APIs](https://auth0.com/blog/call-protected-api-from-blazor-web-app/#Call-an-External-API) which requires injecting the access token into HTTP requests. This is handled by the [TokenHandler.cs](https://github.com/grantcolley/atlas-origin/blob/main/src/Atlas.Blazor.Web.App/Authentication/TokenHandler.cs).\n\n```C#\n    public class TokenHandler(IHttpContextAccessor httpContextAccessor) : DelegatingHandler\n    {\n        private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;\n\n        protected override async Task\u003cHttpResponseMessage\u003e SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)\n        {\n            if(_httpContextAccessor.HttpContext == null)  throw new NullReferenceException(nameof(_httpContextAccessor.HttpContext));\n\n            string? accessToken = await _httpContextAccessor.HttpContext.GetTokenAsync(\"access_token\").ConfigureAwait(false);\n\n            request.Headers.Authorization = new AuthenticationHeaderValue(\"Bearer\", accessToken);\n\n            return await base.SendAsync(request, cancellationToken);\n        }\n    }\n```\n\n### Log In\nClicking the `Login` button on the top right corner of the application will re-direct the user to the [Auth0](https://auth0.com/) login page. Once authenticated, the user is directed back to the application, and the navigation panel will display the Modules, Categories and Pages the user has permission to access.\n\nClick the `Login` button on the top right corner to be redirected to the [Auth0](https://auth0.com/) login page.\n\n![Alt text](/readme-images/Login.png?raw=true \"Login\") \n\nAuthenticate in [Auth0](https://auth0.com/).\n\n![Alt text](/readme-images/Auth0_Login.png?raw=true \"Login\") \n\nThe [Auth0](https://auth0.com/) callback redirects the authenticated user back to **Atlas**, and the navigation panel will display the Modules, Categories and Pages the user has permission to access.\n\n![Alt text](/readme-images/Logged_in.png?raw=true \"Logged in\") \n\n# Authorization\n### Users, Roles and Permissions\n**Atlas** users are maintained in the Atlas database. The user's email in the **Atlas** database corresponds to the email claim provided by [Auth0](https://auth0.com/) to authenticated users. When a user is authenticated, a lookup is done in the **Atlas** database to get the users roles and permissions. This will determine which modules, categories and pages the user has access to in the navigation panel. It will also provide more granular permissions in each rendered page e.g. read / write.\n\nCreating, updating and deleting **Atlas** users, roles and permissions, is done in the `Authorisation` category of the `Administration` module.\n\n\u003e [!TIP]\n\u003e The `Authorisation` category of the `Administration` module is only accessible to users who are members of the `Admin-Read Role` and `Admin-Write Role`.\n\u003e The `Admin-Read Role` gives read-only view of users, roles and permissions, while the `Admin-Write Role` permit creating, updating and deleting them.\n\n![Alt text](/readme-images/Users.png?raw=true \"Users\") \n\nHere we see user Bob is assignd the roles `Support Role` and `User Role`.\n\n![Alt text](/readme-images/User_Bob.png?raw=true \"Bob the Support user\") \n\nHere we see the role `Support Role`, the list of permissions it has been granted, and we can see Bob is a member of the role.   \n\n![Alt text](/readme-images/Role_Support.png?raw=true \"The Support role\") \n\nHere we see the permission `Support`, and the roles that have been granted the `Support` permission.\n\n![Alt text](/readme-images/Permission_Support.png?raw=true \"The Support permission\") \n\n# Navigation\n### Modules, Categories and Pages\nModules are applications, and can be related or unrelated to each other. Each module consists of one or more categories. Each category groups related pages. A page is a routable razor `@page`.\n\nCreating, updating and deleting modules, categories and pages, is done in the `Applications` category of the `Administration` module.\n\n\u003e [!TIP]\n\u003e Because each page must point to a routable razor `@page`, the `Applications` category of the `Administration` module is only accessible to users who are members of the `Developer Role`.\n\u003e i.e. creating, updating and deleting modules, categories and pages is a developer concern.\n\nEach module, category and page in the Navigation panel has a permission, and are only accessible to users who have been assigned that permission via role membership.\n\n![Alt text](/readme-images/Modules.png?raw=true \"Modules\") \n\nHere we see the `Support` module, the order it appears in the navigation panel, the permission required for it to appear in the navigation panel, and the icon that is displayed with it in the navigation panel. We see it has an `Events` category. We can also see highlighted in yellow how it appears in the navigation panel.\n\n![Alt text](/readme-images/Module_Support.png?raw=true \"Support module\") \n\nHere we see the `Events` category, the module it belongs to, the order it appears under the module in the navigation panel, the permission required for it to appear in the navigation panel, and the icon that is displayed with it in the navigation panel. We also see it has a page called `Logs`.\n\n![Alt text](/readme-images/Category_Events.png?raw=true \"Events category\") \n\nHere we see the `Logs` page, the category it belongs to, the order it appears under the category in the navigation panel, the permission required for it to appear in the navigation panel, and the icon that is displayed with it in the navigation panel. Crucially, we also see the route, which is the routable razor `@page` that it navigates to when the user clicks the page in the navigation panel.\n\n![Alt text](/readme-images/Page_Logs.png?raw=true \"Logs page\") \n\nHere we can see the [Logs.razor](https://github.com/grantcolley/atlas/blob/59fb7ab83b40ceb90424168541be41fab11c64a1/src/Atlas.Blazor.Web/Pages/Support/Logs.razor#L1) component, with its routable `@page`  attribute.\n\n\u003e [!IMPORTANT]  \n\u003e The route specified in the page must map to a valid `@page` attribute on a routable component.\n\n```HTML+Razor\n@page \"/Logs\"\n@using System.Text.Json\n@rendermode @(new InteractiveServerRenderMode(prerender: false))\n@attribute [StreamRendering]\n\n\u003cPageTitle\u003eLogs\u003c/PageTitle\u003e\n\n@if (_alert == null)\n{\n    \u003cFluentCard\u003e\n        \u003cFluentHeader\u003e\n            Logs\n        \u003c/FluentHeader\u003e\n\n\u003c!-- code removed for brevity --\u003e\n\n```\n\n# Support\n\n\u003e [!TIP]\n\u003e The Support module, its categories and routable pages, are only accessible to users who are members of the `Support Role`.\n\n\u003e [!NOTE]\n\u003e Members of the `Support Role` also have `Admin-Read` and `Admin-Write` permissions, permitting them to add, update and delete users.\n\n### Logging\nLogs are persisted to the `Logs` table in the **Atlas** database and are viewable to members of the `Support Role`. \n\nHere we can see mock logs created at startup when `\"GenerateSeedLogs\":  \"true\"` is set in the **Atlas.API**'s [appsettings.json](https://github.com/grantcolley/atlas/blob/84031b9b572082965a6668c6e57ce8f0b61d3f86/src/Atlas.API/appsettings.json#L53).\n\n![Alt text](/readme-images/Logs.png?raw=true \"Logs\") \n\nClicking on the log entry will display the full log details in a popup box. \n\n![Alt text](/readme-images/Log_Dialog.png?raw=true \"Log dialog\") \n\n# Audit\nThe [ApplicationDbContext.cs](https://github.com/grantcolley/atlas/blob/3c93993b057c5c7d12ea1e8eb081f0496596ab18/src/Atlas.Data.Access/Context/ApplicationDbContext.cs#L79-L226) uses EF Change Tracking to capture `OldValue` and `NewValues` from `INSERT`'s, `UPDATE`'s and `DELETE`'s, for entities where their poco model class inherits from [ModelBase.cs](https://github.com/grantcolley/atlas/blob/main/src/Atlas.Core/Models/ModelBase.cs). Tracked changes can be queried in the **Audit** table of the **Atlas** database.\n\n![Alt text](/readme-images/Audits.png?raw=true \"Audits\") \n\nMore can be read here about change tracking in Entity Framework:\n- [Change Tracking in EF Core](https://learn.microsoft.com/en-us/ef/core/change-tracking/)\n- [Change Detection and Notifications](https://learn.microsoft.com/en-us/ef/core/change-tracking/change-detection)\n- [Tracking Changes of Entities in EF Core](https://www.entityframeworktutorial.net/efcore/changetracker-in-ef-core.aspx)\n\n## Publish Atlas to Azure\nFirst, create an Azure account.\n\n#### Steps to Publish Atlas to Azure\n#### Resource Group\nCreate a **Resource Group** called `atlas-rg` and select an appropriate **Region**.\n\n![Alt text](/readme-images/Azure/Create_ResourceGroup.png?raw=true \"Create a Resource Group\")\n\n#### Web App + Database\nIn the **Resource Group**, create a **Web App + Database** called `atlas-web-api`.\n- Select the same **Region** as the **Resource Group**.\n- Select the **Runtime stack**.\n- Stick with the `SQLAzure` **Engine**, and default server and database names provided. \n- For the **Hosting plan**, select `Basic - For hobby or research purposes`.\n\n![Alt text](/readme-images/Azure/Create_WebAppAndDatabase.png?raw=true \"Create WebApp + Database\")\n\n#### Web App\nIn the **Resource Group**, create a **Web App** called `atlas-blazor`.\n- Select the **Runtime stack**.\n- For **Operating system** select `Linux`.\n- Select the same **Region** as the **Resource Group**.\n- Skip **Database**, in the **Deployment** section ensure **Basic authentication** is selected, and keep the rest of the default settings.\n\n![Alt text](/readme-images/Azure/Create_WebApp_Blazor.png?raw=true \"Create WebApp - Blazor\")\n\n#### Environment Variables\nGo to `atlas-web-api` resource.\nFor this demo we will use the config in the `appsettings.Production.json` file for the **Atlas.API** and **Atlas.Blazor.Web.App** projects.\n- In **Environment variables** -\u003e **Connection strings**, copy the connection string from the environment variable `AZURE_SQL_CONNECTIONSTRING`, and paste it into `AtlasConnection` in the `appsettings.Production.json` file for both projects.  \n- Delete environment variable `AZURE_SQL_CONNECTIONSTRING`. \n- Click **Apply** -\u003e **Confirm**\n\n#### Auth0\nLogin to `Auth0` and:\n- add the `atlas-blazor` url to `Allowed Callback URLs` e.g. `{atlas-blazor url here...}/callback`\n- add the `atlas-blazor` url to `Allowed Logout URLs` e.g. `{atlas-blazor url here...}/`\n\n![Alt text](/readme-images/Azure/auth0-application-uris.png?raw=true \"Auth0 Application URIs\") \n\n#### Update Web API Configuration and Publish to Azure\nIn the **Atlas.API** project's `appsettings.Production.json` update the following:\n```json\n{\n  \"ConnectionStrings\": {\n    \"AtlasConnection\": \"\" // 👈 connection string to atlas-web-api-database\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Microsoft.EntityFrameworkCore.Database.Command\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Auth0\": {\n    \"Domain\": \"\", // 👈 Auth0 domain\n    \"Audience\": \"https://Atlas.API.com\"\n  },\n  \"CorsOrigins\": {\n    \"Policy\": \"atlas-cors-policy\",\n    \"Urls\": \"\" // 👈 url to atlas-blazor app \n  },\n  \"Database\": {\n    \"MigrateDatabase\": \"true\",\n    \"CreateDatabase\": \"true\",\n    \"GenerateSeedData\": \"true\",\n    \"GenerateSeedLogs\": \"false\"\n  }\n}\n```\n\n- Right click the project and select **Publish**.\n- For **Target** select `Azure`. \n- For **Specific target** select `Azure App Service (Linux)`.\n- For **App Service** select `atlas-web-api`.\n- For **API Management** tick `Skip this step`.\n- For **Deployment type** select `Publish (generates pubxml file)`.\n- In the projects **Properties** -\u003e **PublishProfiles** folder, add the following to `atlas-web-api - Zip Deploy.pubxml`.\n```xml\n  \u003cItemGroup\u003e\n    \u003cContent Update=\"appsettings.json\"\u003e\n     \u003cCopyToPublishDirectory\u003eNever\u003c/CopyToPublishDirectory\u003e\n     \u003cCopyToOutputDirectory\u003eNever\u003c/CopyToOutputDirectory\u003e\n    \u003c/Content\u003e\t  \n    \u003cContent Update=\"appsettings.Development.json\"\u003e\n     \u003cCopyToPublishDirectory\u003eNever\u003c/CopyToPublishDirectory\u003e\n     \u003cCopyToOutputDirectory\u003eNever\u003c/CopyToOutputDirectory\u003e\n    \u003c/Content\u003e\n  \u003c/ItemGroup\u003e\n```\n\nIn the Publish tab click the publish button. This will build and publish **Atlas.API** to `atlas-web-api`. A new tab will open in your default browser for the running `atlas-web-api`. Append `/swagger/index.html` to the url and hit enter.\n\n![Alt text](/readme-images/Azure/atlas-web-api-swagger.png?raw=true \"Atlas Web API Swagger\")\n\n#### Update Blazor Web App Configuration and Publish to Azure\n7. In the **Atlas.Blazor.Web.App** project's `appsettings.Production.json` update the following:\n```json\n{\n  \"ConnectionStrings\": {\n    \"AtlasConnection\": \"\" // 👈 connection string to atlas-web-api-database\n  },\n  \"Logging\": {\n    \"LogLevel\": {\n      \"Default\": \"Information\",\n      \"Microsoft.AspNetCore\": \"Warning\",\n      \"Microsoft.EntityFrameworkCore.Database.Command\": \"Warning\"\n    }\n  },\n  \"AllowedHosts\": \"*\",\n  \"Auth0\": {\n    \"Domain\": \"\", // 👈 Auth0 domain\n    \"ClientId\": \"\", // 👈 Auth0 ClientId\n    \"ClientSecret\": \"\", // 👈 Auth0 ClientSecret\n    \"Audience\": \"https://Atlas.API.com\"\n  },\n  \"AtlasAPI\": \"\" // 👈 url to atlas-web-api \n}\n```\n- Right click the project and select **Publish**.\n- For **Target** select `Azure`. \n- For **Specific target** select `Azure App Service (Linux)`.\n- For **App Service** select `atlas-blazor`.\n- For **Deployment type** select `Publish (generates pubxml file)`.\n- In the projects **Properties** -\u003e **PublishProfiles** folder, add the following to `atlas-blazor - Zip Deploy.pubxml`.\n```xml\n  \u003cItemGroup\u003e\n    \u003cContent Update=\"appsettings.json\"\u003e\n     \u003cCopyToPublishDirectory\u003eNever\u003c/CopyToPublishDirectory\u003e\n     \u003cCopyToOutputDirectory\u003eNever\u003c/CopyToOutputDirectory\u003e\n    \u003c/Content\u003e\t  \n    \u003cContent Update=\"appsettings.Development.json\"\u003e\n     \u003cCopyToPublishDirectory\u003eNever\u003c/CopyToPublishDirectory\u003e\n     \u003cCopyToOutputDirectory\u003eNever\u003c/CopyToOutputDirectory\u003e\n    \u003c/Content\u003e\n  \u003c/ItemGroup\u003e\n```\n\nIn the Publish tab click the publish button. This will build and publish **Atlas.Blazor.Web.App** to `atlas-blazor`. A new tab will open in your default browser for the running `atlas-blazor`.\n\n![Alt text](/readme-images/Azure/atlas-blazor-login.png?raw=true \"Atlas Blazor Login\")\n\nClick `login` to be re-directed to `Auth0` for authentication.\n\n![Alt text](/readme-images/Azure/Auth0_Login_Blank.png?raw=true \"Auth0 Login\")\n\n`Auth0` will re-direct bck to the `callback` page.\n\n![Alt text](/readme-images/Azure/atlas-blazor-loggedin.png?raw=true \"Atlas Web API Swagger\")\n\n# Worked Examples\n## Blazor Template\nCreate a Blazor Template module for the standard template WeatherForecast and Counter pages.\n\n1. Add a new permission constant in **Atlas.Core**'s `Auth` class.\n```C#\n    public static class Auth\n    {\n        public const string ATLAS_USER_CLAIM = \"atlas-user\";\n        public const string ADMIN_READ = \"Admin-Read\";\n        public const string ADMIN_WRITE = \"Admin-Write\";\n        public const string DEVELOPER = \"Developer\";\n        public const string SUPPORT = \"Support\";\n        public const string BLAZOR_TEMPLATE = \"Blazor-Template\"; // 👈 new Blazor-Template permission \n    }\n```\n\n2. Create a new `WeatherForecast` class in **Atlas.Core**'s `Models` folder.\n```C#\n    public class WeatherForecast\n    {\n        public DateOnly Date { get; set; }\n        public int TemperatureC { get; set; }\n        public string? Summary { get; set; }\n        public int TemperatureF =\u003e 32 + (int)(TemperatureC / 0.5556);\n    }\n```\n\n3. Create a new `WeatherEndpoints` class in **Atlas.API**'s `Endpoints` folder.\n```C#\n    public class WeatherEndpoints\n    {\n        internal static async Task\u003cIResult\u003e GetWeatherForecast(IClaimData claimData, IClaimService claimService, ILogService logService, CancellationToken cancellationToken)\n        {\n            Authorisation? authorisation = null;\n\n            try\n            {\n                authorisation = await claimData.GetAuthorisationAsync(claimService.GetClaim(), cancellationToken)\n                    .ConfigureAwait(false);\n\n                if (authorisation == null\n                    || !authorisation.HasPermission(Auth.BLAZOR_TEMPLATE))\n                {\n                    return Results.Unauthorized();\n                }\n\n                var startDate = DateOnly.FromDateTime(DateTime.Now);\n                var summaries = new[] { \"Freezing\", \"Bracing\", \"Chilly\", \"Cool\", \"Mild\", \"Warm\", \"Balmy\", \"Hot\", \"Sweltering\", \"Scorching\" };\n                IEnumerable\u003cWeatherForecast\u003e forecasts = Enumerable.Range(1, 5).Select(index =\u003e new WeatherForecast\n                {\n                    Date = startDate.AddDays(index),\n                    TemperatureC = Random.Shared.Next(-20, 55),\n                    Summary = summaries[Random.Shared.Next(summaries.Length)]\n                });\n\n                return Results.Ok(forecasts);\n            }\n            catch (AtlasException ex)\n            {\n                logService.Log(Core.Logging.Enums.LogLevel.Error, ex.Message, ex, authorisation?.User);\n\n                return Results.StatusCode(StatusCodes.Status500InternalServerError);\n            }\n        }\n    }\n```\n\n4. Add a new constant for the weather forecast endpoint in **Atlas.Core**'s `AtlasAPIEndpoints` class.\n```C#\n    public static class AtlasAPIEndpoints\n    {\n        // existing code removed for brevity\n\n        public const string GET_WEATHER_FORECAST = \"getweatherforecast\"; // 👈 new getweatherforecast endpoint constant \n    }\n```\n\n5. Map the weather forecast endpoint in **Atlas.API**'s `ModulesEndpointMapper` class.\n```C#\n    internal static class ModulesEndpointMapper\n    {\n        internal static WebApplication? MapAtlasModulesEndpoints(this WebApplication app)\n        {\n            // Additional module API's mapped here...\n\n            app.MapGet($\"/{AtlasAPIEndpoints.GET_WEATHER_FORECAST}\", WeatherEndpoints.GetWeatherForecast)\n                .WithOpenApi()\n                .WithName(AtlasAPIEndpoints.GET_WEATHER_FORECAST)\n                .WithDescription(\"Gets the weather forecast\")\n                .Produces\u003cIEnumerable\u003cWeatherForecast\u003e?\u003e(StatusCodes.Status200OK)\n                .Produces(StatusCodes.Status500InternalServerError)\n                .RequireAuthorization(Auth.ATLAS_USER_CLAIM);\n\n            return app;\n        }\n    }\n```\n\n6. Create a new interface `IWeatherForecastRequests` in **Atlas.Requests**'s `Interfaces` folder.\n```C#\n    public interface IWeatherForecastRequests\n    {\n        Task\u003cIEnumerable\u003cWeatherForecast\u003e?\u003e GetWeatherForecastAsync();\n    }\n```\n\n7. Create a new class `WeatherForecastRequests` in **Atlas.Requests**'s `API` folder.\n```C#\n    public class WeatherForecastRequests(HttpClient httpClient) : IWeatherForecastRequests\n    {\n        protected readonly HttpClient _httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));\n        protected readonly JsonSerializerOptions _jsonSerializerOptions = new(JsonSerializerDefaults.Web);\n\n        public async Task\u003cIEnumerable\u003cWeatherForecast\u003e?\u003e GetWeatherForecastAsync()\n        {\n            return await JsonSerializer.DeserializeAsync\u003cIEnumerable\u003cWeatherForecast\u003e?\u003e\n                (await _httpClient.GetStreamAsync(AtlasAPIEndpoints.GET_WEATHER_FORECAST)\n                .ConfigureAwait(false), _jsonSerializerOptions).ConfigureAwait(false);\n        }\n    }\n```\n\n8. Register the `WeatherForecastRequests` service in **Atlas.Blazor.Web.App**'s `Program` file.\n```C#\n   // existing code removed for brevity\n   \n   builder.Services.AddTransient\u003cIWeatherForecastRequests, WeatherForecastRequests\u003e(sp =\u003e\n   {\n       IHttpClientFactory httpClientFactory = sp.GetRequiredService\u003cIHttpClientFactory\u003e();\n       HttpClient httpClient = httpClientFactory.CreateClient(AtlasWebConstants.ATLAS_API);\n       return new WeatherForecastRequests(httpClient);\n   });\n\n   WebApplication app = builder.Build();\n\n   // existing code removed for brevity\n```\n\n9. Create a new Blazor `Weather` component in **Atlas.Blazor.Web**'s `/Components/Pages` folder\n```HTML+Razor\n@page \"/Weather\"\n@attribute [StreamRendering]\n@rendermode @(new InteractiveServerRenderMode(prerender: false))\n\n\u003cPageTitle\u003eWeather\u003c/PageTitle\u003e\n\n\u003cFluentLabel Typo=\"Typography.PageTitle\"\u003eWeather\u003c/FluentLabel\u003e\n\n\u003cbr\u003e\n\n\u003cFluentLabel Typo=\"Typography.PaneHeader\"\u003eThis component demonstrates showing data.\u003c/FluentLabel\u003e\n\n\u003cbr\u003e\n\n@if (Forecasts == null)\n{\n    \u003cFluentLabel Typo=\"Typography.Subject\"\u003eLoading...\u003c/FluentLabel\u003e\n}\nelse\n{\n    \u003cFluentDataGrid TGridItem=Atlas.Core.Models.WeatherForecast Items=\"@Forecasts\"\n                     Style=\"height: 600px;overflow:auto;\" GridTemplateColumns=\"0.25fr 0.25fr 0.25fr 0.25fr\"\n                     ResizableColumns=true GenerateHeader=\"GenerateHeaderOption.Sticky\"\u003e\n        \u003cPropertyColumn Property=\"@(f =\u003e f.Date.ToShortDateString())\" Title=\"Date\" Sortable=\"true\" /\u003e\n        \u003cPropertyColumn Property=\"@(f =\u003e f.TemperatureC)\" Sortable=\"true\" /\u003e\n        \u003cPropertyColumn Property=\"@(f =\u003e f.TemperatureF)\" Sortable=\"true\" /\u003e\n        \u003cPropertyColumn Property=\"@(f =\u003e f.Summary)\" Sortable=\"true\" /\u003e\n    \u003c/FluentDataGrid\u003e\n}\n\n@code {\n    [Inject]\n    public IWeatherForecastRequests? WeatherForecastRequests { get; set; }\n\n    private IEnumerable\u003cWeatherForecast\u003e? _forecasts;\n\n    public IQueryable\u003cWeatherForecast\u003e? Forecasts\n    {\n        get\n        {\n            return _forecasts?.AsQueryable();\n        }\n    }\n\n    protected override async Task OnInitializedAsync()\n    {\n        if (WeatherForecastRequests == null) throw new NullReferenceException(nameof(WeatherForecastRequests));\n\n        _forecasts = await WeatherForecastRequests.GetWeatherForecastAsync().ConfigureAwait(false);\n    }\n}\n```\n\n10. Create a new Blazor `Counter` component in **Atlas.Blazor.Web**'s `/Components/Pages` folder.\n```HTML+Razor\n@page \"/Counter\"\n@rendermode InteractiveAuto\n\n\u003cPageTitle\u003eCounter\u003c/PageTitle\u003e\n\n\u003cFluentLabel Typo=\"Typography.PageTitle\"\u003eCounter\u003c/FluentLabel\u003e\n\n\u003cbr\u003e\n\n\u003cFluentLabel Typo=\"Typography.PaneHeader\"\u003eCurrent count: @currentCount\u003c/FluentLabel\u003e\n\n\u003cbr\u003e\n\n\u003cFluentButton Appearance=\"Appearance.Accent\" OnClick=\"@IncrementCount\"\u003eClick me\u003c/FluentButton\u003e\n\n@code {\n    private int currentCount = 0;\n\n    private void IncrementCount()\n    {\n        currentCount++;\n    }\n}\n```\n\n11. Create a new user `will@email.com` in Auth0 and assign the user the `atlas-user` role.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Auth0_User.png?raw=true \"Blazor Template Auth0 New User\")\n\n12. Create the permission `Blazor-Template`.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Permission.png?raw=true \"Blazor-Template permission\")\n\n13. Create the role `Blazor Template Role` and assign it the `Blazor-Template` permission.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Role.png?raw=true \"Blazor Template role\")\n\n14. Create the user `will@email.com` and assign the `Blazor Template Role` role.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_User.png?raw=true \"Blazor Template user\")\n\n15. Create the module `Blazor Template`.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Module.png?raw=true \"Blazor Template module\")\n\n16. Create the category `Templates` and set the Module to `Blazor Template`.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Category.png?raw=true \"Blazor Template category\")\n\n17. Create the page `Weather` and set the Category to `Templates`.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Page_Weather.png?raw=true \"Blazor Template weather\")\n\n18. Create the page `Counter` and set the Category to `Templates`.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Page_Counter.png?raw=true \"Blazor Template counter\")\n\n19. Log out, then log back in as user `will@email.com`\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Auth0_Login.png?raw=true \"Blazor Template Auth0 login\")\n\n20. In the navigation panel, user `will@email.com` only has permission to the Blazor Template module.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Home.png?raw=true \"Blazor Template Home\")\n\n21. Click on the Weather navigation link.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Component_Weather.png?raw=true \"Blazor Template Weather\")\n\n22. Click on the Counter navigation link.\n![Alt text](/readme-images/BlazorTemplate/Blazor_Template_Component_Counter.png?raw=true \"Blazor Template Counter\")\n\n# Notes\n### FluentDesignTheme Dark/Light\nWhat the [Fluent UI quick guide](https://fluentui-blazor.net/DesignTheme) doesn't tell you is you must also add a reference to `/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css`.\n\nFor the Blazor Web App project, add the reference to the top of the `app.css` file in `wwwroot`:\n```C#\n@import '/_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css';\n```\n\nFor the Blazor WebAssembly stand alone project, add the reference to the `index.html` file in `wwwroot`.\n```C#\n\u003cLink href=\"_content/Microsoft.FluentUI.AspNetCore.Components/css/reboot.css\" rel=\"stylesheet\" /\u003e\n```\n\n### Backend for frontend\n- [Calling a Protected API from a Blazor Web App](https://auth0.com/blog/call-protected-api-from-blazor-web-app/)\n- [Backend for Frontend (BFF)](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-browser-based-apps#name-backend-for-frontend-bff)\n- [Microsoft Architecture Patterns - Backends for Frontends](https://learn.microsoft.com/en-us/azure/architecture/patterns/backends-for-frontends)\n\n\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrantcolley%2Fatlas","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgrantcolley%2Fatlas","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgrantcolley%2Fatlas/lists"}