Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added docs/screenshots/map-redesign/01-default-map.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
24 changes: 24 additions & 0 deletions framework/SimpleModule.Hosting/CspOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace SimpleModule.Hosting;

/// <summary>
/// Configurable Content Security Policy directives. Modules that need external
/// resources (tile servers, CDNs) can append origins at startup via
/// <c>builder.AddSimpleModule(o => o.Csp.ConnectSources.Add("https://tiles.example.com"))</c>.
/// </summary>
public class CspOptions
{
/// <summary>Extra origins appended to <c>connect-src</c>.</summary>
public List<string> ConnectSources { get; } = [];

/// <summary>Extra origins appended to <c>img-src</c>.</summary>
public List<string> ImgSources { get; } = [];

/// <summary>Extra origins appended to <c>worker-src</c>.</summary>
public List<string> WorkerSources { get; } = [];

/// <summary>Extra origins appended to <c>font-src</c>.</summary>
public List<string> FontSources { get; } = [];

/// <summary>Extra origins appended to <c>style-src</c>.</summary>
public List<string> StyleSources { get; } = [];
}
42 changes: 23 additions & 19 deletions framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,24 @@ public static async Task UseSimpleModuleInfrastructure(this WebApplication app)

app.UseHttpsRedirection();
var isDevelopment = app.Environment.IsDevelopment();
var cspOptions = options.Csp;

// Directives never change after startup, so build everything except the
// per-request nonce once. Per request we only do a single concat.
var connectSrc = isDevelopment
? $"'self' ws: wss: https: {string.Join(' ', cspOptions.ConnectSources)}"
: $"'self' https: {string.Join(' ', cspOptions.ConnectSources)}";

var cspPrefix = "default-src 'none'; script-src 'self' 'nonce-";
var cspSuffix =
$"'; style-src 'self' 'unsafe-inline' fonts.googleapis.com rsms.me {string.Join(' ', cspOptions.StyleSources)}; "
+ $"font-src 'self' fonts.gstatic.com rsms.me {string.Join(' ', cspOptions.FontSources)}; "
+ $"worker-src 'self' blob: {string.Join(' ', cspOptions.WorkerSources)}; "
+ $"connect-src {connectSrc}; "
+ $"img-src 'self' data: https: {string.Join(' ', cspOptions.ImgSources)}; "
+ "object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none';";
var cspSuffixHttps = cspSuffix + " upgrade-insecure-requests;";

app.Use(
async (context, next) =>
{
Expand All @@ -181,25 +199,11 @@ public static async Task UseSimpleModuleInfrastructure(this WebApplication app)
headers["X-Frame-Options"] = "SAMEORIGIN";
headers["Referrer-Policy"] = "strict-origin-when-cross-origin";
headers["X-Permitted-Cross-Domain-Policies"] = "none";
// In development, allow WebSocket connections for live reload
var connectSrc = isDevelopment ? "'self' ws: wss:" : "'self'";
var csp =
$"default-src 'none'; "
+ $"script-src 'self' 'nonce-{nonce}'; "
+ $"style-src 'self' 'unsafe-inline' fonts.googleapis.com rsms.me; "
+ $"font-src 'self' fonts.gstatic.com rsms.me; "
+ $"connect-src {connectSrc}; "
+ $"img-src 'self' data:; "
+ $"object-src 'none'; "
+ $"base-uri 'self'; "
+ $"form-action 'self'; "
+ $"frame-ancestors 'none';";
if (isHttps)
{
csp += " upgrade-insecure-requests;";
}

headers["Content-Security-Policy"] = csp;
headers["Content-Security-Policy"] = string.Concat(
cspPrefix,
nonce,
isHttps ? cspSuffixHttps : cspSuffix
);
return Task.CompletedTask;
});
await next();
Expand Down
6 changes: 6 additions & 0 deletions framework/SimpleModule.Hosting/SimpleModuleOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ public class SimpleModuleOptions

public bool EnableDevTools { get; set; } = true;

/// <summary>
/// Content Security Policy overrides. Modules can append extra origins for
/// directives like <c>connect-src</c>, <c>img-src</c>, etc.
/// </summary>
public CspOptions Csp { get; } = new();

/// <summary>
/// The detected database provider, set during startup validation.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,38 @@ namespace SimpleModule.Map.EntityConfigurations;

public class BasemapConfiguration : IEntityTypeConfiguration<Basemap>
{
/// <summary>
/// Fixed ids for the basemaps seeded via <see cref="EntityTypeBuilder.HasData"/>.
/// Reference these from other seeders that link back to this catalog.
/// </summary>
public static class SeedIds
{
public static readonly BasemapId MapLibreDemotiles = BasemapId.From(
new Guid("22222222-2222-2222-2222-000000000001")
);
public static readonly BasemapId OpenFreeMapLiberty = BasemapId.From(
new Guid("22222222-2222-2222-2222-000000000002")
);
public static readonly BasemapId OpenFreeMapPositron = BasemapId.From(
new Guid("22222222-2222-2222-2222-000000000003")
);
public static readonly BasemapId OpenFreeMapBright = BasemapId.From(
new Guid("22222222-2222-2222-2222-000000000004")
);
public static readonly BasemapId VersatilesColorful = BasemapId.From(
new Guid("22222222-2222-2222-2222-000000000005")
);

public static IReadOnlyList<BasemapId> All { get; } =
[
MapLibreDemotiles,
OpenFreeMapLiberty,
OpenFreeMapPositron,
OpenFreeMapBright,
VersatilesColorful,
];
}

public void Configure(EntityTypeBuilder<Basemap> builder)
{
builder.HasKey(b => b.Id);
Expand All @@ -30,7 +62,7 @@ private static Basemap[] GenerateSeedBasemaps()
[
new Basemap
{
Id = BasemapId.From(new Guid("22222222-2222-2222-2222-000000000001")),
Id = SeedIds.MapLibreDemotiles,
Name = "MapLibre Demotiles",
Description = "Official MapLibre demo vector style. Free for development.",
StyleUrl = "https://demotiles.maplibre.org/style.json",
Expand All @@ -41,7 +73,7 @@ private static Basemap[] GenerateSeedBasemaps()
},
new Basemap
{
Id = BasemapId.From(new Guid("22222222-2222-2222-2222-000000000002")),
Id = SeedIds.OpenFreeMapLiberty,
Name = "OpenFreeMap Liberty",
Description = "OpenFreeMap free vector basemap, Liberty style.",
StyleUrl = "https://tiles.openfreemap.org/styles/liberty",
Expand All @@ -52,7 +84,7 @@ private static Basemap[] GenerateSeedBasemaps()
},
new Basemap
{
Id = BasemapId.From(new Guid("22222222-2222-2222-2222-000000000003")),
Id = SeedIds.OpenFreeMapPositron,
Name = "OpenFreeMap Positron",
Description = "OpenFreeMap free vector basemap, light Positron style.",
StyleUrl = "https://tiles.openfreemap.org/styles/positron",
Expand All @@ -63,7 +95,7 @@ private static Basemap[] GenerateSeedBasemaps()
},
new Basemap
{
Id = BasemapId.From(new Guid("22222222-2222-2222-2222-000000000004")),
Id = SeedIds.OpenFreeMapBright,
Name = "OpenFreeMap Bright",
Description = "OpenFreeMap free vector basemap, Bright style.",
StyleUrl = "https://tiles.openfreemap.org/styles/bright",
Expand All @@ -74,7 +106,7 @@ private static Basemap[] GenerateSeedBasemaps()
},
new Basemap
{
Id = BasemapId.From(new Guid("22222222-2222-2222-2222-000000000005")),
Id = SeedIds.VersatilesColorful,
Name = "Versatiles Colorful",
Description = "VersaTiles free OSM-based vector basemap, Colorful style.",
StyleUrl = "https://tiles.versatiles.org/assets/styles/colorful/style.json",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,35 @@ namespace SimpleModule.Map.EntityConfigurations;

public class LayerSourceConfiguration : IEntityTypeConfiguration<LayerSource>
{
/// <summary>
/// Fixed ids for layer sources seeded via <see cref="EntityTypeBuilder.HasData"/>.
/// Reference these from other seeders that link back to this catalog.
/// </summary>
public static class SeedIds
{
public static readonly LayerSourceId OpenStreetMapXyz = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000001")
);
public static readonly LayerSourceId TerrestrisOsmWms = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000002")
);
public static readonly LayerSourceId TerrestrisTopoWms = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000003")
);
public static readonly LayerSourceId MapLibreDemotilesVector = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000004")
);
public static readonly LayerSourceId ProtomapsFirenzePmTiles = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000005")
);
public static readonly LayerSourceId GeomaticoKrigingCog = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000006")
);
public static readonly LayerSourceId MapLibreEarthquakesGeoJson = LayerSourceId.From(
new Guid("11111111-1111-1111-1111-000000000007")
);
}

/// <summary>
/// Toggles mapping of the spatial <see cref="LayerSource.Coverage"/> column.
/// Defaults to <c>false</c>; flipped on by <c>MapModule.ConfigureServices</c>
Expand Down Expand Up @@ -84,7 +113,7 @@ private static LayerSource[] GenerateSeedSources()
// ── Raster basemaps (XYZ) ────────────────────────────────────────────
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000001")),
Id = SeedIds.OpenStreetMapXyz,
Name = "OpenStreetMap (raster tiles)",
Description =
"Standard OSM raster tiles. Free for low-volume use; respect the OSMF tile usage policy.",
Expand All @@ -102,7 +131,7 @@ private static LayerSource[] GenerateSeedSources()
// ── WMS (terrestris demo, used in the official MapLibre WMS example) ─
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000002")),
Id = SeedIds.TerrestrisOsmWms,
Name = "terrestris OSM-WMS",
Description =
"Public WMS by terrestris. Used in the official MapLibre 'Add a WMS source' example.",
Expand All @@ -124,7 +153,7 @@ private static LayerSource[] GenerateSeedSources()
},
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000003")),
Id = SeedIds.TerrestrisTopoWms,
Name = "terrestris TOPO-WMS",
Description = "terrestris topographic WMS overlay layer (transparent).",
Type = LayerSourceType.Wms,
Expand All @@ -146,7 +175,7 @@ private static LayerSource[] GenerateSeedSources()
// ── Vector tiles (MapLibre demotiles) ────────────────────────────────
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000004")),
Id = SeedIds.MapLibreDemotilesVector,
Name = "MapLibre demotiles (vector)",
Description = "Official MapLibre demo MVT vector tileset. Free for development.",
Type = LayerSourceType.VectorTile,
Expand All @@ -163,7 +192,7 @@ private static LayerSource[] GenerateSeedSources()
// ── PMTiles (Protomaps demo archive used in MapLibre PMTiles example) ─
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000005")),
Id = SeedIds.ProtomapsFirenzePmTiles,
Name = "Protomaps Firenze (PMTiles)",
Description =
"Public PMTiles vector archive of Florence (ODbL). Used in the MapLibre PMTiles example.",
Expand All @@ -183,7 +212,7 @@ private static LayerSource[] GenerateSeedSources()
// ── COG (geomatico demo Cloud-Optimized GeoTIFF) ─────────────────────
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000006")),
Id = SeedIds.GeomaticoKrigingCog,
Name = "Geomatico kriging COG (demo)",
Description =
"Public Cloud-Optimized GeoTIFF demo from the maplibre-cog-protocol sample viewer.",
Expand All @@ -199,7 +228,7 @@ private static LayerSource[] GenerateSeedSources()
// ── GeoJSON (raw OSM Overpass-style demo: world airports subset) ─────
new LayerSource
{
Id = LayerSourceId.From(new Guid("11111111-1111-1111-1111-000000000007")),
Id = SeedIds.MapLibreEarthquakesGeoJson,
Name = "MapLibre demotiles point sample (GeoJSON)",
Description =
"Small public GeoJSON FeatureCollection from the MapLibre demo assets.",
Expand Down
28 changes: 28 additions & 0 deletions modules/Map/src/SimpleModule.Map/MapService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using SimpleModule.Core.Exceptions;
using SimpleModule.Datasets.Contracts;
using SimpleModule.Map.Contracts;
using SimpleModule.Map.EntityConfigurations;

namespace SimpleModule.Map;

Expand Down Expand Up @@ -193,6 +194,33 @@ public async Task<SavedMap> GetDefaultMapAsync(CancellationToken ct = default)
Pitch = Options.DefaultPitch,
Bearing = Options.DefaultBearing,
BaseStyleUrl = Options.BaseStyleUrl,
Basemaps = BasemapConfiguration
.SeedIds.All.Select((id, i) => new MapBasemap { BasemapId = id, Order = i })
.ToList(),
Layers =
[
new MapLayer
{
LayerSourceId = LayerSourceConfiguration.SeedIds.OpenStreetMapXyz,
Order = 0,
Visible = true,
Opacity = 1,
},
new MapLayer
{
LayerSourceId = LayerSourceConfiguration.SeedIds.MapLibreEarthquakesGeoJson,
Order = 1,
Visible = true,
Opacity = 1,
},
new MapLayer
{
LayerSourceId = LayerSourceConfiguration.SeedIds.TerrestrisOsmWms,
Order = 2,
Visible = false,
Opacity = 1,
},
],
};

db.SavedMaps.Add(seed);
Expand Down
Loading
Loading