Compare commits
6 Commits
a2b40910c2
...
main
Author | SHA1 | Date | |
---|---|---|---|
55778b8fb8 | |||
a19c3bcff4 | |||
af884061b6 | |||
df8f43ba5a | |||
140e1db769 | |||
d139b94a62 |
@ -36,6 +36,7 @@ export default defineConfig({
|
||||
{ text: 'Google Sign in without Identity', link: '/dotnet/google-sign-in-without-identity' },
|
||||
{ text: 'Service Testing', link: '/dotnet/service-testing' },
|
||||
{ text: 'Controller Testing', link: '/dotnet/controller-testing' },
|
||||
{ text: 'API Key Authentication', link: '/dotnet/api-key-auth'}
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -43,6 +44,7 @@ export default defineConfig({
|
||||
link: '/angular/',
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Application Settings', link: '/angular/application-settings' },
|
||||
{ text: 'Base Components', link: '/angular/base-components' },
|
||||
]
|
||||
},
|
||||
@ -70,6 +72,8 @@ export default defineConfig({
|
||||
collapsed: true,
|
||||
items: [
|
||||
{ text: 'Docker Exec', link: '/docker/exec-into-container' },
|
||||
{ text: 'Local DB (MSSQL)', link: '/docker/local-db-mssql' },
|
||||
{ text: 'Local DB (PostgreSQL)', link: '/docker/local-db-pg' },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
87
docs/angular/application-settings.md
Normal file
87
docs/angular/application-settings.md
Normal file
@ -0,0 +1,87 @@
|
||||
# Application Configuration
|
||||
|
||||
Often you will need to use some kind of configuration in your application. For most backends this can be done using envionment variables or settings files, however in Angular applications this is not as straight forward.
|
||||
A common approach I like to take is to setup a `appsettings.json` file, that is served alonside the application and can be used to store configuration values. The application will read this file, pass it around as a provider and use it as needed.
|
||||
|
||||
## Setup
|
||||
Start by creating a new file in the `public` directory called `appsettings.json`. This file will contain all the configuration values for the application.
|
||||
|
||||
```json
|
||||
{
|
||||
"someSetting": "SomeValue"
|
||||
}
|
||||
```
|
||||
|
||||
Next we will update the `main.ts` to read the `appsettings.json` file and pass it to the application as a provider. Setup the following exports before the application bootstrap.
|
||||
|
||||
```typescript
|
||||
export const APP_SETTINGS = new InjectionToken<AppSettings>('APP_SETTINGS');
|
||||
|
||||
export const fetchAppSettings = async (): Promise<AppSettings> => {
|
||||
const baseHref = document.querySelector('base')?.getAttribute('href');
|
||||
const settingUrl = baseHref === '/' ? '/appsettings.json' : `${baseHref}/appsettings.json`;
|
||||
const response = await fetch(settingUrl);
|
||||
return await response.json();
|
||||
};
|
||||
|
||||
export const provideAppSettings = (appSettings: AppSettings): StaticProvider => {
|
||||
return {
|
||||
provide: APP_SETTINGS,
|
||||
useValue: appSettings,
|
||||
};
|
||||
};
|
||||
```
|
||||
In this we are creating a new `InjectionToken` called `APP_SETTINGS` that will be used to inject the configuration values into the application. We also have a function `fetchAppSettings` that will fetch the `appsettings.json` file and return the configuration values. Finally we have a function `provideAppSettings` that will create a provider for the configuration values.
|
||||
|
||||
Next we can update the bootstrap function to fetch the configuration values and pass them to the application.
|
||||
|
||||
```typescript
|
||||
(async function () {
|
||||
// Fetch the app settings and then bootstrap the application
|
||||
const appSettings = await fetchAppSettings();
|
||||
bootstrapApplication(AppComponent, {
|
||||
providers: [
|
||||
provideAppSettings(appSettings),
|
||||
{ provide: appConfig, useValue: appSettings },
|
||||
...appConfig.providers,
|
||||
],
|
||||
})
|
||||
})();
|
||||
```
|
||||
|
||||
::: tip
|
||||
If you have other configuration in the appConfig you will also need to ensure it's passed correctly to the `bootstrapApplication()` function.
|
||||
:::
|
||||
|
||||
This will fetch the configuration values and pass them to the application as a provider.
|
||||
|
||||
## Usage
|
||||
|
||||
Using settings is now as simple as injecting the `APP_SETTINGS` token into a service or component.
|
||||
|
||||
::: code-group
|
||||
|
||||
```typescript [app.component.ts]
|
||||
import { Component, inject } from '@angular/core';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { APP_SETTINGS } from '../main';
|
||||
import { AppSettings } from './common/models/config';
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
standalone: true,
|
||||
imports: [RouterOutlet],
|
||||
templateUrl: './app.component.html',
|
||||
styleUrl: './app.component.scss'
|
||||
})
|
||||
export class AppComponent {
|
||||
appSettings: AppSettings = inject(APP_SETTINGS);
|
||||
title = 'app-settings';
|
||||
someSetting = this.appSettings.someSetting
|
||||
}
|
||||
```
|
||||
|
||||
```html [app.component.html]
|
||||
<h1>{{ title }}</h1>
|
||||
<p>The value of someSettings is: {{ someSetting }}</p>
|
||||
```
|
||||
:::
|
@ -1,3 +1,4 @@
|
||||
# Angular Snippets and Musings
|
||||
|
||||
#### [Application Settings](./application-settings.md)
|
||||
#### [Base Components](./base-components.md)
|
@ -1,3 +1,5 @@
|
||||
# Docker Snippets and Musings
|
||||
|
||||
#### [Exec Into Container](./exec-into-container.md)
|
||||
#### [Local Database With Scripts (MSSQL)](./local-db-mssql.md)
|
||||
#### [Local Database With Scripts (PostgreSQL)](./local-db-pg.md)
|
174
docs/docker/local-db-mssql.md
Normal file
174
docs/docker/local-db-mssql.md
Normal file
@ -0,0 +1,174 @@
|
||||
# Local Database With Scripts (MSSQL)
|
||||
|
||||
When developing apps locally it can be really useful to have a dockerised database unique to the application.
|
||||
|
||||
Often rather than just running a pre-built image, you'll want to run a database with some initial data, tables, or a schema.
|
||||
|
||||
For this purpose we can create our own image that extends the base image and adds our own scripts.
|
||||
|
||||
## Setup
|
||||
|
||||
For most applications the directory structure will look something like this:
|
||||
|
||||
```
|
||||
database/
|
||||
Dockerfile
|
||||
scripts/
|
||||
01-create-database.sql
|
||||
02-create-tables.sql
|
||||
03-seed-data.sql
|
||||
development/
|
||||
compose.yml
|
||||
src/
|
||||
...
|
||||
tests/
|
||||
...
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Create a dockerfile in the `database/` directory:
|
||||
|
||||
::: code-group
|
||||
|
||||
```dockerfile [Dockerfile]
|
||||
FROM mcr.microsoft.com/mssql/server:2022-latest
|
||||
|
||||
# Set the SQL Server environment variables
|
||||
ENV ACCEPT_EULA="Y"
|
||||
ENV SA_PASSWORD="Password123"
|
||||
|
||||
# Setup port
|
||||
EXPOSE 1433
|
||||
|
||||
# Create a temp directory
|
||||
RUN mkdir -p /tmp/init
|
||||
|
||||
# Copy all the scripts into the container
|
||||
COPY ./scripts/ /tmp/init
|
||||
|
||||
ENTRYPOINT [ "/tmp/init/entrypoint.sh" ]
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: danger
|
||||
|
||||
As this is a local development database, we're using the `sa` user with a simple password. **Do not use this in production**.
|
||||
|
||||
:::
|
||||
|
||||
### Scripts
|
||||
|
||||
Create the scripts in the `database/scripts/` directory:
|
||||
|
||||
::: code-group
|
||||
|
||||
```bash [entrypoint.sh]
|
||||
#!/bin/bash
|
||||
|
||||
# Run the sql scripts and start sql server
|
||||
/tmp/init/run-scripts.sh & /opt/mssql/bin/sqlservr
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: code-group
|
||||
|
||||
```bash [run-scripts.sh]
|
||||
#!/bin/bash
|
||||
|
||||
# Wait for the mssql database to be ready
|
||||
while ! /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -Q "SELECT 1" > /dev/null; do
|
||||
sleep 1
|
||||
done
|
||||
|
||||
echo "SQL Server is up and running"
|
||||
|
||||
# Check if the setup has already been executed
|
||||
SETUP_DONE=$(/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P "$SA_PASSWORD" -C -Q "IF EXISTS (SELECT 1 FROM master.sys.tables WHERE name = 'setup_marker' AND schema_id = SCHEMA_ID('dbo')) SELECT 1 ELSE SELECT 0" -h -1 -W -r 1 | grep -oE '^[0-9]+' | tr -d '[:space:]')
|
||||
|
||||
if [[ "$SETUP_DONE" == "1" ]]; then
|
||||
echo "Setup has already been completed. Skipping initialization."
|
||||
exit 0
|
||||
else
|
||||
echo "Setup has not been completed. Running initialization."
|
||||
fi
|
||||
|
||||
# Run all scripts in the scripts folder
|
||||
for entry in /tmp/init/*.sql;
|
||||
do
|
||||
echo "Running script $entry"
|
||||
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -i $entry
|
||||
done
|
||||
|
||||
# Create a marker table to indicate setup completion
|
||||
/opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P $SA_PASSWORD -C -Q "CREATE TABLE master.dbo.setup_marker (id INT PRIMARY KEY IDENTITY, created_at DATETIME DEFAULT GETDATE())"
|
||||
|
||||
echo "All scripts have been run"
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
The above script waits for the database to be ready, then checks if the setup has already run. If not it will run all the scripts in the `scripts/` directory and create a marker table to indicate that the setup has been completed.
|
||||
|
||||
Create any scripts that you need in the `database/scripts/` directory.
|
||||
|
||||
::: tip
|
||||
|
||||
See below for an example of the scripts you might want to run.
|
||||
|
||||
:::
|
||||
|
||||
::: code-group
|
||||
|
||||
```sql [01-create-database.sql]
|
||||
CREATE DATABASE MyDatabase
|
||||
```
|
||||
|
||||
```sql [02-create-tables.sql]
|
||||
USE MyDatabase
|
||||
|
||||
CREATE TABLE MyTable (
|
||||
id INT PRIMARY KEY,
|
||||
name NVARCHAR(50)
|
||||
)
|
||||
```
|
||||
|
||||
```sql [03-seed-data.sql]
|
||||
USE MyDatabase
|
||||
|
||||
INSERT INTO MyTable (id, name) VALUES (1, 'Alice')
|
||||
INSERT INTO MyTable (id, name) VALUES (2, 'Bob')
|
||||
```
|
||||
|
||||
```sql [04-create-user.sql]
|
||||
USE MyDatabase
|
||||
|
||||
CREATE LOGIN MyUser WITH PASSWORD = 'MyPassword'
|
||||
CREATE USER MyUser FOR LOGIN MyUser
|
||||
ALTER ROLE db_owner ADD MEMBER MyUser;
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Compose
|
||||
|
||||
Lastly we need to create a `docker-compose.yml` file in the `development/` directory:
|
||||
|
||||
::: code-group
|
||||
|
||||
```yaml [compose.yml]
|
||||
services:
|
||||
db:
|
||||
build:
|
||||
context: ../database
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- db-data:/var/opt/mssql
|
||||
ports:
|
||||
- "1433:1433"
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
```
|
89
docs/docker/local-db-pg.md
Normal file
89
docs/docker/local-db-pg.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Local Database With Scripts (PostgreSQL)
|
||||
|
||||
When developing apps locally it can be really useful to have a dockerised database unique to the application.
|
||||
|
||||
Often rather than just running a pre-built image, you'll want to run a database with some initial data, tables, or a schema.
|
||||
|
||||
For this purpose we can create our own image that extends the base image and adds our own scripts.
|
||||
|
||||
## Setup
|
||||
|
||||
For most applications the directory structure will look something like this:
|
||||
|
||||
```
|
||||
database/
|
||||
Dockerfile
|
||||
scripts/
|
||||
01-create-tables.sql
|
||||
development/
|
||||
compose.yml
|
||||
src/
|
||||
...
|
||||
tests/
|
||||
...
|
||||
```
|
||||
|
||||
### Dockerfile
|
||||
|
||||
Create a dockerfile in the `database/` directory:
|
||||
|
||||
::: code-group
|
||||
|
||||
```dockerfile [Dockerfile]
|
||||
FROM postgres:17
|
||||
|
||||
# Setup the postgres environment variables
|
||||
ENV POSTGRES_USER=myuser
|
||||
ENV POSTGRES_PASSWORD=mypassword
|
||||
ENV POSTGRES_DB=mydatabase
|
||||
|
||||
# Setup port
|
||||
EXPOSE 5432
|
||||
|
||||
# Copy all the scripts into the container
|
||||
COPY ./scripts /docker-entrypoint-initdb.d/
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
::: danger
|
||||
|
||||
As this is a local development database, we're using the a simple username and password. **Do not use this in production**.
|
||||
|
||||
:::
|
||||
|
||||
### Scripts
|
||||
|
||||
Create any scripts you need in the `database/scripts/` directory. PostgreSQL will run these scripts in alphabetical order against the database specified in the `POSTGRES_DB` environment variable.
|
||||
|
||||
::: code-group
|
||||
|
||||
```sql [01-create-tables.sql]
|
||||
CREATE TABLE MyTable (
|
||||
Id INT NOT NULL PRIMARY KEY,
|
||||
Name VARCHAR(50) NOT NULL
|
||||
);
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Compose
|
||||
|
||||
Lastly we need to create a `docker-compose.yml` file in the `development/` directory:
|
||||
|
||||
::: code-group
|
||||
|
||||
```yaml [compose.yml]
|
||||
services:
|
||||
db:
|
||||
build:
|
||||
context: ../database
|
||||
dockerfile: Dockerfile
|
||||
volumes:
|
||||
- db-data:/var/lib/postgresql/data
|
||||
ports:
|
||||
- "5432:5432"
|
||||
|
||||
volumes:
|
||||
db-data:
|
||||
```
|
136
docs/dotnet/api-key-auth.md
Normal file
136
docs/dotnet/api-key-auth.md
Normal file
@ -0,0 +1,136 @@
|
||||
# API Key Auth
|
||||
|
||||
Simple API Key authentication is a great option when building public facing APIs without strict security requirements, but you would rather not leave open. Think syncs, long running jobs or other non-critical operations.
|
||||
|
||||
## Configuration
|
||||
|
||||
This example stores the ApiKey in the `appsettings.json` file. You can also store it in a database, environment variable, or any other configuration source.
|
||||
|
||||
::: code-group
|
||||
|
||||
```json[appsettings.json]
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
"ApiKey": "ThisIsMySecretKey",
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Filter
|
||||
|
||||
The logic for the api key authentication is a simple Authorization filter. It checks the `ApiKey` header against the configured value.
|
||||
|
||||
Start by storing the header name in a constants file or similar:
|
||||
|
||||
::: code-group
|
||||
|
||||
```csharp[Constants.cs]
|
||||
namespace ApiKeyAuthDemo.Core
|
||||
{
|
||||
public static class Constants
|
||||
{
|
||||
public const string API_KEY_HEADER_NAME = "X-API-KEY";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
Then create the filter:
|
||||
|
||||
::: code-group
|
||||
|
||||
```csharp[ApiKeyAuthorizeAttribute.cs]
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc.Filters;
|
||||
|
||||
namespace ApiKeyAuthDemo.Core.Filters
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
|
||||
public class ApiKeyAuthorizeAttribute() : Attribute, IAuthorizationFilter
|
||||
{
|
||||
public void OnAuthorization(AuthorizationFilterContext context)
|
||||
{
|
||||
// Get the API key from the request headers
|
||||
string? apiKeyValue = context.HttpContext.Request.Headers[Constants.API_KEY_HEADER_NAME];
|
||||
|
||||
// Get the API key from the configuration
|
||||
IConfiguration configuration = context.HttpContext.RequestServices.GetRequiredService<IConfiguration>();
|
||||
string? apiKey = configuration.GetValue<string>("ApiKey");
|
||||
|
||||
// Check if the API key is valid and set
|
||||
if (apiKeyValue == null || apiKeyValue != apiKey)
|
||||
{
|
||||
context.Result = new UnauthorizedResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
||||
|
||||
## Usage
|
||||
|
||||
See below for example usage (on the second GET method):
|
||||
|
||||
::: code-group
|
||||
|
||||
```csharp[WeatherForecastController.cs]
|
||||
using ApiKeyAuthDemo.Core.Filters;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
|
||||
namespace ApiKeyAuthDemo.Controllers;
|
||||
|
||||
[ApiController]
|
||||
[Route("[controller]")]
|
||||
public class WeatherForecastController : ControllerBase
|
||||
{
|
||||
private static readonly string[] Summaries = new[]
|
||||
{
|
||||
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
|
||||
};
|
||||
|
||||
private readonly ILogger<WeatherForecastController> _logger;
|
||||
|
||||
public WeatherForecastController(ILogger<WeatherForecastController> logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public IEnumerable<WeatherForecast> Get()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
[ApiKeyAuthorize]
|
||||
[HttpGet("auth")]
|
||||
public IEnumerable<WeatherForecast> GetAuth()
|
||||
{
|
||||
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
|
||||
{
|
||||
Date = DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
|
||||
TemperatureC = Random.Shared.Next(-20, 55),
|
||||
Summary = Summaries[Random.Shared.Next(Summaries.Length)]
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::
|
@ -12,3 +12,4 @@
|
||||
#### [Google Sign in Without Identity](./google-sign-in-without-identity.md)
|
||||
#### [Service Testing](./service-testing.md)
|
||||
#### [Controller Testing](./controller-testing.md)
|
||||
#### [API Key Authentication](./api-key-auth.md)
|
Reference in New Issue
Block a user