Compare commits

...

2 Commits

Author SHA1 Message Date
beo3000 6b2715cec9 added deplayment-scripts
fix club-permissions
2025-12-31 18:08:32 +01:00
beo3000 6c82d16ddd fix comment 2025-12-30 21:06:06 +01:00
13 changed files with 482 additions and 16 deletions

19
deploy/create_sp.ps1 Normal file
View File

@ -0,0 +1,19 @@
# Variablen
$spName = "sp-koogle-prod"
$subscriptionId = "f1332eb1-392f-4b16-9ede-7905848ef248"
$tenantId = "94cf90d7-e9ff-49a1-bc3b-a5b94d3cc8ca" # Optional; kann auch aus Ausgabe ermittelt werden
# Service Principal erstellen (Single-Tenant, Passwort/Secret)
$sp = az ad sp create-for-rbac `
--name $spName `
--role Contributor `
--scopes /subscriptions/$subscriptionId `
--years 1 `
--query "{appId:appId, tenant:tenant, password:password}" -o json | ConvertFrom-Json
Write-Host "SP erstellt:"
Write-Host " appId: $($sp.appId)"
Write-Host " tenant: $($sp.tenant)"
Write-Host " secret: $($sp.password)" # Speichere sicher (z. B. 1Password/Key Vault)

45
deploy/env.ps1 Normal file
View File

@ -0,0 +1,45 @@
# ====== Globale Variablen ======
# Service Prinvipal
# appId: bc062e91-deb8-4516-b3c8-ad0a0886f297
# tenant: 94cf90d7-e9ff-49a1-bc3b-a5b94d3cc8ca
# secret: nTB8Q~D4MVWWE0ub19ZIu8e-bKV9b087Ptz4ideg
# Variante 1: Client-Secret
$spAppId = "bc062e91-deb8-4516-b3c8-ad0a0886f297" # aus create-for-rbac (appId)
$spClientSecret = "nTB8Q~D4MVWWE0ub19ZIu8e-bKV9b087Ptz4ideg" # aus create-for-rbac (password)
# Variante 2: Zertifikat (PEM mit Zertifikat+PrivateKey)
# $spAppId = "<SP_APP_ID>"
# $spCertPath = "C:\pfad\zertifikat+privatekey.pem"
# Azure Basis
$subscriptionId = "f1332eb1-392f-4b16-9ede-7905848ef248" # Deine Subscription
$location = "westeurope"
# Ressourcennamen (eindeutig)
$rgName = "rg-koogle-prod"
$appPlanName = "asp-koogle-prod"
$webAppName = "KoogleWeb20251231130827" # global eindeutig
$sqlServerName = "sql-koogle-prod" # global eindeutig
$sqlDbName = "db-koogle"
$keyVaultName = "kv-koogle-prod" # global eindeutig
$appInsightsName = "ai-koogle-prod"
# Custom Domain
$domain = "koogle.straso.com" # Ziel-Subdomain
# App/Projekt
$projectPath = "src/Koogle.Web/Koogle.Web.csproj" # Pfad zu .csproj
$runtimeStack = "DOTNETCORE:9.0" # Linux .NET 9 - prüfen, welche Pläne verfügbar sind: az webapp list-runtimes --os linux
# AAD Admin für SQL Server (bestehender Benutzer oder Gruppe)
# ObjectId = ID aus Entra ID; Display = Anzeigename
$aadAdminObjectId = "70f83097-e12a-48a6-96b4-15760268c994" # für SQL AAD-Admin (admin-d-chrka@krah-gruppe.de)
$aadAdminDisplay = "Admin D-Chrka" # z.B. 'Christian Kauer'

120
deploy/infra.ps1 Normal file
View File

@ -0,0 +1,120 @@
write-host "1:" . $PSScriptRoot
write-host "2:" . $(Get-Location)
# $test = $(Get-Location)
$EnvPath = "$PSScriptRoot/env.ps1"
# ===== 1) Env laden =====
if (-not (Test-Path -Path $EnvPath)) {
throw "Env-Datei '$EnvPath' wurde nicht gefunden. Bitte Pfad prüfen."
} else {
Write-Host "ENV-File found: $EnvPath"
}
Write-Host "🧩 Lade Umgebungsvariablen aus: $EnvPath" -ForegroundColor Cyan
. $EnvPath # dot-source: lädt Variablen in den aktuellen Scope
# ===== 2) Pflichtwerte validieren =====
$missing = @()
if ([string]::IsNullOrWhiteSpace($subscriptionId)) { $missing += 'subscriptionId' }
if ([string]::IsNullOrWhiteSpace($rgName)) { $missing += 'rgName' }
if ([string]::IsNullOrWhiteSpace($appPlanName)) { $missing += 'appPlanName' }
if ([string]::IsNullOrWhiteSpace($webAppName)) { $missing += 'webAppName' }
if ([string]::IsNullOrWhiteSpace($sqlServerName)) { $missing += 'sqlServerName' }
if ([string]::IsNullOrWhiteSpace($sqlDbName)) { $missing += 'sqlDbName' }
if ([string]::IsNullOrWhiteSpace($keyVaultName)) { $missing += 'keyVaultName' }
if ([string]::IsNullOrWhiteSpace($appInsightsName)) { $missing += 'appInsightsName' }
if ([string]::IsNullOrWhiteSpace($runtimeStack)) { $missing += 'runtimeStack' }
if ([string]::IsNullOrWhiteSpace($projectPath)) { $missing += 'projectPath' }
if ([string]::IsNullOrWhiteSpace($location)) { $missing += 'location' }
if ([string]::IsNullOrWhiteSpace($aadAdminObjectId)) { $missing += 'aadAdminObjectId' }
if ([string]::IsNullOrWhiteSpace($aadAdminDisplay)) { $missing += 'aadAdminDisplay' }
if ($missing.Count -gt 0) {
throw "Fehlende Pflichtvariablen in '$EnvPath': $($missing -join ', ')"
}
$hasSpAppId = -not [string]::IsNullOrWhiteSpace($spAppId)
$hasSpClientSecret = -not [string]::IsNullOrWhiteSpace($spClientSecret)
$hasSpCertPath = -not [string]::IsNullOrWhiteSpace($spCertPath)
# ===== Login & Subscription setzen =====
Write-Host "Cloud auf AzureCloud setzen..." -ForegroundColor Cyan
az cloud set --name AzureCloud | Out-Null # Sicherheits-Check gegen falsche Cloud [3](https://moldstud.com/articles/p-mastering-azure-sql-databases-a-comprehensive-guide-to-creating-and-managing-with-azure-cli)
if ($hasSpAppId -and ($hasSpClientSecret -or $hasSpCertPath)) {
Write-Host "Azure Login (Service Principal)..." -ForegroundColor Cyan
if ($spClientSecret) {
az login --service-principal --username $spAppId --password $spClientSecret --tenant $tenantId | Out-Null
} else {
az login --service-principal --username $spAppId --certificate $spCertPath --tenant $tenantId | Out-Null
}
} else {
Write-Host "Azure Login (Benutzer, Tenant fixiert)..." -ForegroundColor Cyan
az login --tenant $tenantId | Out-Null
}
#Write-Host "Subscription setzen: $subscriptionId" -ForegroundColor Cyan
#az account set --subscription $subscriptionId | Out-Null # aktive Subscription definieren [1](https://learn.microsoft.com/en-us/azure/app-service/tutorial-connect-msi-azure-database)[4](https://learn.microsoft.com/en-us/azure/app-service/tutorial-connect-app-access-sql-database-as-user-dotnet)
# Ressourcengruppe
#az group create -n $RgName -l $Location
# App Service Plan (Linux, B1 reicht meist für wenige User)
#az appservice plan create -n $AppPlanName -g $RgName --is-linux --sku B1
# Web App (Linux, .NET 9)
#az webapp create -g $RgName -p $AppPlanName -n $WebAppName --runtime "$RuntimeStack"
# System-assigned Managed Identity aktivieren
az webapp identity assign -g $RgName -n $WebAppName
$principalId = az webapp show -g $RgName -n $WebAppName --query identity.principalId -o tsv
# Azure SQL Server + DB
az sql server create -g $RgName -n $SqlServerName -l $Location -u "sqladmin" -p (New-Guid).Guid
az sql db create -g $RgName -s $SqlServerName -n $SqlDbName --service-objective S0
# Azure AD Admin auf SQL Server setzen (damit AAD-basierte User & MI erstellt werden können)
az sql server ad-admin create `
-g $RgName -s $SqlServerName `
--display-name $AadAdminDisplay `
--object-id $AadAdminObjectId
# (CLI-Referenz zu 'az sql server ad-admin': siehe Doku) # [2](https://learn.microsoft.com/en-us/cli/azure/sql/server/ad-admin?view=azure-cli-latest)
# Firewall-Regel für dein Notebook (lokaler Zugriff/Tools wie DataGrip)
$myIp = (Invoke-RestMethod -Uri "https://api.ipify.org")
az sql server firewall-rule create -g $RgName -s $SqlServerName -n "allow-notebook" --start-ip-address $myIp --end-ip-address $myIp
# Key Vault
az keyvault create -n $KeyVaultName -g $RgName -l $Location
# WebApp Managed Identity -> Key Vault: Secret-Rechte
az keyvault set-policy -n $KeyVaultName -g $RgName --object-id $principalId --secret-permissions get list
# App Insights (optional, empfohlen)
az monitor app-insights component create -g $RgName -l $Location -a $AppInsightsName
$ikey = az monitor app-insights component show -g $RgName -a $AppInsightsName --query instrumentationKey -o tsv
# App Settings
az webapp config appsettings set -g $RgName -n $WebAppName --settings `
ASPNETCORE_ENVIRONMENT=Production `
APPINSIGHTS_INSTRUMENTATIONKEY=$ikey `
WEBSITE_RUN_FROM_PACKAGE=1
Write-Host "Infrastruktur erstellt. WebApp:" "https://$WebAppName.azurewebsites.net"
Write-Host "Nächster Schritt: DNS CNAME für $domain -> $WebAppName.azurewebsites.net setzen und TXT (asuid) zur Verifizierung im Azure Portal abrufen."

View File

@ -0,0 +1,4 @@
$ProgressPreference = 'SilentlyContinue'
Invoke-WebRequest -Uri https://aka.ms/installazurecliwindows -OutFile .\AzureCLI.msi
Start-Process msiexec.exe -Wait -ArgumentList '/I', 'AzureCLI.msi', '/quiet'
Remove-Item .\AzureCLI.msi

View File

@ -20,20 +20,28 @@ create migrations for AppDbContext
```
dotnet ef migrations add [Initial_App] --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppDbContext --output-dir Data/Migrations
```
update database:
update database DEV:
```
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext
```
update database PROD:
```
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppDbContext --connection "Data Source=sql-koogle-prod.database.windows.net;Initial Catalog=db-koogle;User ID=sqladmin;Password=Magister7000#;Connect Timeout=30;Encrypt=True;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
```
### AppIdentityDbContext
create migrations for AppIdentityDbContext:
```
dotnet ef migrations add [Initial_Auth] --project src/Koogle.Infrastructure --startup-project src/Koogle.Web --context AppIdentityDbContext --output-dir Identity/Migrations
```
update database:
update database DEV:
```
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppIdentityDbContext
```
update database PROD:
```
dotnet ef database update -p src/Koogle.Infrastructure -s src/Koogle.Web --context AppIdentityDbContext --connection "Data Source=sql-koogle-prod.database.windows.net;Initial Catalog=db-koogle;User ID=sqladmin;Password=Magister7000#;Connect Timeout=30;Encrypt=True;Trust Server Certificate=False;Application Intent=ReadWrite;Multi Subnet Failover=False"
```
# Tests

View File

@ -139,12 +139,13 @@ public static class DemoSeeder
}
// 7) Seed template GIFs if not already present
var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
if (!hasGifs)
{
await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName);
}
//var webEnv = scope.ServiceProvider.GetRequiredService<IWebHostEnvironment>();
//var hasGifs = await appDb.ClubGifs.AnyAsync(g => g.ClubId == club.Id && !g.IsDeleted);
//if (!hasGifs)
//{
// await SeedTemplateGifsInternalAsync(appDb, webEnv, club.Id, club.LoginName);
//}
}
/// <summary>

View File

@ -24,7 +24,7 @@
@if (AuthState.Value.IsClubEditor || AuthState.Value.IsSuperAdmin)
@if (AuthState.Value.IsClubAdmin || AuthState.Value.IsSuperAdmin)
{
<MudNavGroup Title="Stammdaten"
Icon="@Icons.Material.Filled.Settings"

View File

@ -116,14 +116,77 @@
}
</MudPaper>
<MudPaper Class="pa-4" Elevation="2">
<MudPaper Class="pa-4 mb-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Sicherheit</MudText>
<MudButton Href="/account/forgot-password"
Variant="Variant.Outlined"
Color="Color.Primary">
Passwort aendern
Variant="Variant.Outlined"
Color="Color.Primary">
Passwort ändern
</MudButton>
</MudPaper>
<MudPaper Class="pa-4" Elevation="2">
<MudText Typo="Typo.h6" Class="mb-3">Berechtigungen</MudText>
@foreach (var role in @AuthState.Value.Roles)
{
<MudText>Rolle: @role</MudText>
}
@if (AuthState.Value.IsSuperAdmin)
{
<MudTooltip Text="kann Clubs verwalten">
<MudText>Super-Admin: <MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" /></MudText>
</MudTooltip>
}
else
{
<MudTooltip Text="kann Clubs verwalten">
<MudText>Super-Admin: <MudIcon Icon="@Icons.Material.Filled.DoNotDisturb" Color="Color.Error" /></MudText>
</MudTooltip>
}
<br/>
@if (AuthState.Value.IsClubAdmin)
{
<MudTooltip Text="kann Club-Stammdaten erfassen">
<MudText>Club-Admin: <MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" /></MudText>
</MudTooltip>
}
else
{
<MudTooltip Text="kann Club-Stammdaten erfassen">
<MudText>Club-Admin (kann Club-Stammdaten erfassen): <MudIcon Icon="@Icons.Material.Filled.DoNotDisturb" Color="Color.Error" /> (kann Clubs verwalten)</MudText>
</MudTooltip>
}
<br />
@if (AuthState.Value.IsClubEditor)
{
<MudTooltip Text="kann Spieltage und Spiele anlegen">
<MudText>Club-Admin (): <MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" /></MudText>
</MudTooltip>
}
else
{
<MudTooltip Text="kann Spieltage und Spiele anlegen">
<MudText>Club-Admin: <MudIcon Icon="@Icons.Material.Filled.DoNotDisturb" Color="Color.Error" /></MudText>
</MudTooltip>
}
<br />
@if (AuthState.Value.IsClubViewer)
{
<MudTooltip Text="kann Clubdaten einsehen">
<MudText>Club-Viewer: <MudIcon Icon="@Icons.Material.Filled.Check" Color="Color.Success" /></MudText>
</MudTooltip>
}
else
{
<MudTooltip Text="kann Clubdaten einsehen">
<MudText>Club-Viewer: <MudIcon Icon="@Icons.Material.Filled.DoNotDisturb" Color="Color.Error" /></MudText>
</MudTooltip>
}
</MudPaper>
}
</MudContainer>

View File

@ -22,4 +22,16 @@
<ProjectReference Include="..\Koogle.Application\Koogle.Application.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="wwwroot\club-template\gifs\297970f3-1bf1-4090-aa5e-d0f5cea42a93.gif">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\club-template\gifs\44f5bc11-8471-4ca9-b764-58e3929d1215.gif">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
<Content Update="wwwroot\club-template\gifs\giftemplates.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</Content>
</ItemGroup>
</Project>

View File

@ -0,0 +1,173 @@
{
"$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"metadata": {
"_dependencyType": "compute.function.linux.appService"
},
"parameters": {
"resourceGroupName": {
"type": "string",
"defaultValue": "rg-koogle-prod",
"metadata": {
"description": "Name of the resource group for the resource. It is recommended to put resources under same resource group for better tracking."
}
},
"resourceGroupLocation": {
"type": "string",
"defaultValue": "westeurope",
"metadata": {
"description": "Location of the resource group. Resource groups could have different location than resources, however by default we use API versions from latest hybrid profile which support all locations for resource types we support."
}
},
"resourceName": {
"type": "string",
"defaultValue": "KoogleWeb20251231130827",
"metadata": {
"description": "Name of the main resource to be created by this template."
}
},
"resourceLocation": {
"type": "string",
"defaultValue": "[parameters('resourceGroupLocation')]",
"metadata": {
"description": "Location of the resource. By default use resource group's location, unless the resource provider is not supported there."
}
}
},
"resources": [
{
"type": "Microsoft.Resources/resourceGroups",
"name": "[parameters('resourceGroupName')]",
"location": "[parameters('resourceGroupLocation')]",
"apiVersion": "2019-10-01"
},
{
"type": "Microsoft.Resources/deployments",
"name": "[concat(parameters('resourceGroupName'), 'Deployment', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"resourceGroup": "[parameters('resourceGroupName')]",
"apiVersion": "2019-10-01",
"dependsOn": [
"[parameters('resourceGroupName')]"
],
"properties": {
"mode": "Incremental",
"expressionEvaluationOptions": {
"scope": "inner"
},
"parameters": {
"resourceGroupName": {
"value": "[parameters('resourceGroupName')]"
},
"resourceGroupLocation": {
"value": "[parameters('resourceGroupLocation')]"
},
"resourceName": {
"value": "[parameters('resourceName')]"
},
"resourceLocation": {
"value": "[parameters('resourceLocation')]"
}
},
"template": {
"$schema": "http://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"resourceGroupName": {
"type": "string"
},
"resourceGroupLocation": {
"type": "string"
},
"resourceName": {
"type": "string"
},
"resourceLocation": {
"type": "string"
}
},
"variables": {
"storage_name": "[toLower(concat('storage', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId))))]",
"appServicePlan_name": "[concat('Plan', uniqueString(concat(parameters('resourceName'), subscription().subscriptionId)))]",
"storage_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Storage/storageAccounts/', variables('storage_name'))]",
"appServicePlan_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/serverFarms/', variables('appServicePlan_name'))]",
"function_ResourceId": "[concat('/subscriptions/', subscription().subscriptionId, '/resourceGroups/', parameters('resourceGroupName'), '/providers/Microsoft.Web/sites/', parameters('resourceName'))]"
},
"resources": [
{
"location": "[parameters('resourceLocation')]",
"name": "[parameters('resourceName')]",
"type": "Microsoft.Web/sites",
"apiVersion": "2015-08-01",
"tags": {
"[concat('hidden-related:', variables('appServicePlan_ResourceId'))]": "empty"
},
"dependsOn": [
"[variables('appServicePlan_ResourceId')]",
"[variables('storage_ResourceId')]"
],
"kind": "functionapp",
"properties": {
"name": "[parameters('resourceName')]",
"kind": "functionapp",
"httpsOnly": true,
"reserved": false,
"serverFarmId": "[variables('appServicePlan_ResourceId')]",
"siteConfig": {
"alwaysOn": true,
"linuxFxVersion": "dotnet|3.1"
}
},
"identity": {
"type": "SystemAssigned"
},
"resources": [
{
"name": "appsettings",
"type": "config",
"apiVersion": "2015-08-01",
"dependsOn": [
"[variables('function_ResourceId')]"
],
"properties": {
"AzureWebJobsStorage": "[concat('DefaultEndpointsProtocol=https;AccountName=', variables('storage_name'), ';AccountKey=', listKeys(variables('storage_ResourceId'), '2017-10-01').keys[0].value, ';EndpointSuffix=', 'core.windows.net')]",
"FUNCTIONS_EXTENSION_VERSION": "~3",
"FUNCTIONS_WORKER_RUNTIME": "dotnet"
}
}
]
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('storage_name')]",
"type": "Microsoft.Storage/storageAccounts",
"apiVersion": "2017-10-01",
"tags": {
"[concat('hidden-related:', concat('/providers/Microsoft.Web/sites/', parameters('resourceName')))]": "empty"
},
"properties": {
"supportsHttpsTrafficOnly": true
},
"sku": {
"name": "Standard_LRS"
},
"kind": "Storage"
},
{
"location": "[parameters('resourceGroupLocation')]",
"name": "[variables('appServicePlan_name')]",
"type": "Microsoft.Web/serverFarms",
"apiVersion": "2015-02-01",
"kind": "linux",
"properties": {
"name": "[variables('appServicePlan_name')]",
"sku": "Standard",
"workerSizeId": "0",
"reserved": true
}
}
]
}
}
}
]
}

View File

@ -440,7 +440,6 @@ public class GameEffects
winnerId));
// Fire expense triggers for special throw events
// Note: Use action.AfterThrowState.BellValue since afterThrowState has BellValue reset to false
await FireThrowTriggersAsync(state, currentPlayerId.Value, action, afterThrowState, dispatcher);
// Record player statistics (game-type independent)

View File

@ -1,4 +1,7 @@
{
"ConnectionStrings": {
"AppDb": "Server=localhost;Database=KoogleApp2;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
},
"Bootstrap": {
"EnableSeeding": true,
"CreateDefaultClub": true,

View File

@ -1,6 +1,25 @@
{
"ConnectionStrings": {
"AppDb": "Server=localhost;Database=KoogleApp2;Trusted_Connection=True;MultipleActiveResultSets=true;TrustServerCertificate=True"
"AppDb": "Data Source=sql-koogle-prod.database.windows.net;Initial Catalog=db-koogle;User ID=sqladmin;Password=Magister7000#;Connect Timeout=30;Encrypt=True;Trust Server Certificate=True;Application Intent=ReadWrite;Multi Subnet Failover=False"
},
"Bootstrap": {
"EnableSeeding": true,
"CreateDefaultClub": true,
"SuperAdmin": {
"Email": "christian@kauer-buchhagen.de",
"UserName": "christian@kauer-buchhagen.de",
"Password": "ADMIN_only3000!"
},
"DefaultClub": {
"Name": "Demo Club"
},
"Demo": {
"Enabled": true,
"Email": "demo@koogle.de",
"Password": "Demo123!",
"ClubName": "Demo Club",
"LoginName": "demo"
}
},
"Logging": {
"LogLevel": {
@ -10,6 +29,6 @@
},
"AllowedHosts": "*",
"AppSettings": {
"SaveOnUndoRedo" : false
"SaveOnUndoRedo": false
}
}