feat: auto token acquisition + import helpers for Ka-Note scripts

- get-token.ps1: MSAL.PS-based token acquisition (browser popup once, then cached refresh token)
- download/upload/unlock.ps1: auto-call get-token.ps1 when no -Token/-env provided
- import-helpers.ps1: reusable functions (Upsert-Context, Add-Topic, Add-HistoryEntry) with correct schema
- import-jf-sysadmins.ps1: idempotent import script using helpers

Fixes:
- Token file no longer stored in work/ (deleted on download)
- Upsert-Context resurrects soft-deleted contexts (500 was caused by uploading topics for deleted context)
- History uses topicId + YYYY-MM-DD date (not contextId + timestamp)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
beo3000 2026-02-21 22:08:12 +01:00
parent 28221232d6
commit 3b6ae8c92b
6 changed files with 379 additions and 7 deletions

View File

@ -80,9 +80,12 @@ if (Test-Path $WorkDir) {
if ($Prod) {
$BaseUrl = "https://ka-note.azurewebsites.net"
$BearerToken = if ($Token) { $Token } else { $env:KA_NOTE_TOKEN }
$BearerToken = if ($Token) { $Token } elseif ($env:KA_NOTE_TOKEN) { $env:KA_NOTE_TOKEN } else {
Write-Host " Acquiring token via MSAL..." -ForegroundColor DarkGray
& "$PSScriptRoot\get-token.ps1"
}
if (-not $BearerToken) {
Write-Err "Prod mode requires a Bearer token. Pass -Token or set `$env:KA_NOTE_TOKEN."
Write-Err "Failed to acquire a Bearer token."
exit 1
}
$Headers = @{ Authorization = "Bearer $BearerToken" }

View File

@ -0,0 +1,56 @@
<#
.SYNOPSIS
Gets a Bearer token for the Ka-Note production API via MSAL.PS.
Installs MSAL.PS automatically if missing.
Uses cached tokens / refresh tokens browser login only needed on first run
or after token cache is cleared.
.OUTPUTS
Writes the access token string to stdout.
.EXAMPLE
$token = & "$PSScriptRoot\get-token.ps1"
#>
param()
$ErrorActionPreference = 'Stop'
$ClientId = '1aba7af7-eec1-4e49-b87e-9f941c0e8630'
$TenantId = '94cf90d7-e9ff-49a1-bc3b-a5b94d3cc8ca'
$Scopes = "api://$ClientId/access"
# --- Ensure MSAL.PS is available ---------------------------------------------
if (-not (Get-Module -ListAvailable -Name 'MSAL.PS')) {
Write-Host " [INFO] Installing MSAL.PS module..." -ForegroundColor DarkGray
Install-Module -Name 'MSAL.PS' -Scope CurrentUser -Force -AllowClobber
}
Import-Module MSAL.PS -ErrorAction Stop
# --- Acquire token -----------------------------------------------------------
$params = @{
ClientId = $ClientId
TenantId = $TenantId
Scopes = $Scopes
}
$result = $null
# 1. Try silent first (uses cached access token or refresh token)
try {
$result = Get-MsalToken @params -Silent 2>$null
} catch {
# No cached token or refresh failed — fall back to interactive
}
# 2. Interactive browser login
if (-not $result) {
Write-Host " [AUTH] Opening browser for login..." -ForegroundColor Yellow
$result = Get-MsalToken @params -Interactive
}
if (-not $result -or -not $result.AccessToken) {
Write-Error "Failed to acquire token."
exit 1
}
# Output only the token (callers capture via $token = & .\get-token.ps1)
Write-Output $result.AccessToken

View File

@ -0,0 +1,160 @@
<#
.SYNOPSIS
Shared helper functions for UpNote Ka-Note bundle imports.
Dot-source this file in import scripts: . "$PSScriptRoot\import-helpers.ps1"
.NOTES
Bundle file formats (as of current schema):
contexts.json array of AgendaContext:
id, name, type (meeting|project|person|company), sortOrder,
updatedAt, deletedAt, version, archivedAt, isFavorite, meta
topics.json array of Topic:
id, contextId, title, body, sortOrder, isPinned,
deletedAt, updatedAt, version
history/<id>.meta.json HistoryEntry (without text):
id, topicId, date (YYYY-MM-DD), sortOrder, linkedContextId,
doneAt, wiedervorlageDate, wiedervorlageResolvedAt,
updatedAt, deletedAt, version
history/<id>.md plain text / markdown content
IMPORTANT:
- History is TOPIC-scoped (topicId), NOT context-scoped.
- History date is "YYYY-MM-DD" (no time part).
- History has no "title" field.
- Soft-deleted contexts cause 500 on upload if new topics reference them
always call Upsert-Context which auto-resurrects deleted contexts.
#>
# Globals set by Initialize-BundleSession
$script:WorkDir = $null
$script:Now = $null
function Initialize-BundleSession {
param([string]$WorkDir)
$script:WorkDir = $WorkDir
$script:Now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
if (-not (Test-Path "$WorkDir\history")) { New-Item -ItemType Directory "$WorkDir\history" | Out-Null }
}
# Upsert a context: creates new OR resurrects if soft-deleted OR skips if active.
# Always bumps version and sets name/type/sortOrder if changed.
function Upsert-Context {
param(
[string]$CtxPath,
[string]$Id,
[string]$Name,
[string]$Type = 'meeting',
[int] $SortOrder = 10,
[bool] $IsFavorite = $true
)
$ctxs = Get-Content $CtxPath -Raw -Encoding UTF8 | ConvertFrom-Json
$existing = $ctxs | Where-Object { $_.id -eq $Id }
if ($existing) {
$wasDeleted = $null -ne $existing.deletedAt
$existing | Add-Member -NotePropertyName deletedAt -NotePropertyValue $null -Force
$existing | Add-Member -NotePropertyName name -NotePropertyValue $Name -Force
$existing | Add-Member -NotePropertyName updatedAt -NotePropertyValue $script:Now -Force
$existing.version++
if ($wasDeleted) {
Write-Host " [CTX] Resurrected: $Id" -ForegroundColor Green
} else {
Write-Host " [CTX] Already active, updated: $Id" -ForegroundColor DarkGray
}
} else {
$new = [PSCustomObject]@{
id = $Id
name = $Name
type = $Type
sortOrder = $SortOrder
updatedAt = $script:Now
deletedAt = $null
version = 1
archivedAt = $null
isFavorite = $IsFavorite
meta = $null
}
$ctxs = @($ctxs) + $new
Write-Host " [CTX] Created: $Id ($Name)" -ForegroundColor Green
}
$ctxs | ConvertTo-Json -Depth 10 | Set-Content $CtxPath -Encoding UTF8
}
# Soft-delete all topics for a context (cleanup orphans or old data)
function Remove-ContextTopics {
param([string]$TopicsPath, [string]$ContextId)
$tops = Get-Content $TopicsPath -Raw -Encoding UTF8 | ConvertFrom-Json
$count = 0
foreach ($t in $tops) {
if ($t.contextId -eq $ContextId -and -not $t.deletedAt) {
$t | Add-Member -NotePropertyName deletedAt -NotePropertyValue $script:Now -Force
$t | Add-Member -NotePropertyName updatedAt -NotePropertyValue $script:Now -Force
$t.version++
$count++
}
}
$tops | ConvertTo-Json -Depth 10 | Set-Content $TopicsPath -Encoding UTF8
if ($count -gt 0) { Write-Host " [TOP] Soft-deleted $count orphan topics for $ContextId" -ForegroundColor DarkGray }
}
# Add a new topic. Returns the topic ID.
function Add-Topic {
param(
[string]$TopicsPath,
[string]$ContextId,
[string]$Title,
[string]$Body = '',
[int] $SortOrder = 0,
[bool] $IsPinned = $false
)
$id = [System.Guid]::NewGuid().ToString()
$tops = Get-Content $TopicsPath -Raw -Encoding UTF8 | ConvertFrom-Json
$new = [PSCustomObject]@{
id = $id
contextId = $ContextId
title = $Title
body = $Body
sortOrder = $SortOrder
isPinned = $IsPinned
deletedAt = $null
updatedAt = $script:Now
version = 1
}
$tops = @($tops) + $new
$tops | ConvertTo-Json -Depth 10 | Set-Content $TopicsPath -Encoding UTF8
Write-Host " [TOP] + $Title" -ForegroundColor DarkGray
return $id
}
# Add a history entry (attached to a topic).
# $Date: 'YYYY-MM-DD'
# $Text: markdown string
function Add-HistoryEntry {
param(
[string]$TopicId,
[string]$Date,
[string]$Text,
[int] $SortOrder = 0
)
$id = [System.Guid]::NewGuid().ToString()
$meta = [PSCustomObject]@{
id = $id
topicId = $TopicId
date = $Date
sortOrder = $SortOrder
linkedContextId = $null
doneAt = $null
wiedervorlageDate = $null
wiedervorlageResolvedAt = $null
updatedAt = $script:Now
deletedAt = $null
version = 1
}
$meta | ConvertTo-Json -Depth 5 | Set-Content "$($script:WorkDir)\history\$id.meta.json" -Encoding UTF8
$Text | Set-Content "$($script:WorkDir)\history\$id.md" -Encoding UTF8
Write-Host " [HIS] + $Date" -ForegroundColor DarkGray
return $id
}

View File

@ -0,0 +1,131 @@
<#
.SYNOPSIS
Imports / re-imports the "JF: Team Sysadmins" context into Ka-Note.
Idempotent: resurrects soft-deleted context, soft-deletes orphan topics,
creates clean topic set and history entries.
.PARAMETER Prod
Target production server.
.PARAMETER Force
Skip upload conflict confirmation.
.EXAMPLE
.\import-jf-sysadmins.ps1
.\import-jf-sysadmins.ps1 -Prod
#>
param(
[switch]$Prod,
[switch]$Force
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot\import-helpers.ps1"
$scripts = $PSScriptRoot
$RepoRoot = Resolve-Path "$PSScriptRoot\..\.."
$workDir = Join-Path $RepoRoot "work"
# ── 1. Download ───────────────────────────────────────────────────────────────
Write-Host "`n[1] Downloading bundle..." -ForegroundColor Cyan
$dlArgs = @('-File', "$scripts\download.ps1", '-Force')
if ($Prod) { $dlArgs += '-Prod' }
& powershell @dlArgs
if ($LASTEXITCODE -ne 0) { throw "Download failed (exit $LASTEXITCODE)" }
Initialize-BundleSession -WorkDir $workDir
$ctxPath = "$workDir\contexts.json"
$topsPath = "$workDir\topics.json"
$ctxId = 'jf-sysadmins'
# ── 2. Upsert context (creates or resurrects soft-deleted) ────────────────────
Write-Host "`n[2] Context..." -ForegroundColor Cyan
Upsert-Context -CtxPath $ctxPath -Id $ctxId -Name 'JF: Team Sysadmins' -SortOrder 20
# ── 3. Soft-delete orphan topics ─────────────────────────────────────────────
Write-Host "`n[3] Cleanup orphan topics..." -ForegroundColor Cyan
Remove-ContextTopics -TopicsPath $topsPath -ContextId $ctxId
# ── 4. Create topics ──────────────────────────────────────────────────────────
Write-Host "`n[4] Creating topics..." -ForegroundColor Cyan
$journalId = Add-Topic $topsPath $ctxId 'Sitzungsprotokoll' `
'Meeting-Notizen chronologisch.' 5 $true
Add-Topic $topsPath $ctxId 'TISAX: Sperren Produktionsrechner' `
"Benutzergruppe kein Internet + VLAN shared`n- EOL-VLAN, SERVICE-ACCOUNT VLAN`n- PC-0259`n- Stas funktioniert nicht" 10 | Out-Null
Add-Topic $topsPath $ctxId 'TISAX: Allgemeine Anforderungen' `
"- Doku Backup-Prozess`n- Situationen simulieren`n- Pen-Test vor TISAX (ueber RES)`n- DocuSnap fuer Notfallhandbuch`n- Anforderung 5.3.2 Netzwerkdienste" 20 | Out-Null
Add-Topic $topsPath $ctxId 'Backup und SAN' `
"- Backup-Probleme DW und BUW (warten auf Ports, Hees, Transaktionslogs, Wartungsjobs)`n- VMware Upgrade HKR: Bootreihenfolge festlegen`n- Rucksicherungstest, Prozess mit Hees beschreiben" 30 | Out-Null
Add-Topic $topsPath $ctxId 'Citrix Farm Erneuerung' `
"- Grundinstallation da, Stefan installiert Programme`n- Sophos Endpoint fehlt`n- GPOs + Netscaler Termin SVA`n- MFA und Citrix: Alternativen SVA ausstehend" 40 | Out-Null
Add-Topic $topsPath $ctxId 'internal.lan / Access-Manager' `
"- Nacharbeiten F+E laufen`n- Dennis + Ossenbuehl starten Vorbereitungen`n- Angebot Access-Manager ausstehend" 50 | Out-Null
Add-Topic $topsPath $ctxId 'Greenbone / Schwachstellenmanagement' `
"- Philip: Analyse-Ergebnis heisst nicht, alles selbst bearbeiten`n- Dokumentation in Arbeit`n- Greenbone ueberwacht auch Citrix-Umgebung (ja)" 60 | Out-Null
Add-Topic $topsPath $ctxId 'Windows-Upgrade und EOL-Systeme' `
"- Win10 bei KAS: isoliert? Hardwareauswahl? Ende 14. Okt.`n- XP-Maschinen fluten Netzwerk`n- Admin-Rechte HKR abschalten`n- Rechner Fertigung: IT-Richtlinie" 70 | Out-Null
Add-Topic $topsPath $ctxId 'Netzwerk und Core-Switch' `
"- Core-Switch Nacharbeiten (VLANs stecken, alten Switch ausbauen)`n- Rückbau IPSec Regeln`n- Aufbau Server-VLAN`n- NAC: Liste Geraete vorbereiten" 80 | Out-Null
Add-Topic $topsPath $ctxId 'DocuSnap und Notfallhandbuch' `
"- Notfallhandbuch HKR`n- DocuSnap: Termin mit Kelobit`n- Verzeichnis Verarbeitungstaetigkeiten`n- V14 eingespielt, Sammler gefixt" 90 | Out-Null
Add-Topic $topsPath $ctxId 'KI-Richtlinie und IT-Governance' `
"- KI-Governance: Philip`n- KI-Workshop: Eray`n- Server kuenftig nur auf Englisch (IT-Richtlinie)`n- KAS: keine Admin-Rechte mehr" 100 | Out-Null
Add-Topic $topsPath $ctxId 'Teams und Kommunikation' `
"- Teams-Probleme HKR: Netzwerkkarten bestellt`n- BZ-Ampere Ticket #507535`n- KRAH-App Service-Status" 110 | Out-Null
Add-Topic $topsPath $ctxId 'Diverses und Aufgaben' `
"- deletion of old user: ZAD, Job einplanen`n- IT Kram WITEC`n- SnipeIT, Teleport, CLM Einfuehrung`n- Passwort-DB ausrollen" 120 | Out-Null
# ── 5. Create history entries (attached to Sitzungsprotokoll) ─────────────────
Write-Host "`n[5] Creating history entries..." -ForegroundColor Cyan
Add-HistoryEntry $journalId '2026-02-20' "## Next`n`n- TISAX: Sperren Produktionsrechner (Benutzergruppe kein Internet + VLAN shared)`n- IT Kram WITEC`n- deletion of old user: ZAD aktivieren, Job einplanen`n- Windows ressourcenhungriger -> Leasing pruefen`n- Ticket #507535 BZ-Ampere" | Out-Null
Add-HistoryEntry $journalId '2026-02-18' "## 2026-02-18`n`n- HKR: Warum 10 separate Banfen?`n- TISAX: Sperren Produktionsrechner`n- Greenbone: Analyse-Ergebnis heisst nicht, dass Philip alle bearbeitet`n- Ticket LQS, alte SharePoint Daten" | Out-Null
Add-HistoryEntry $journalId '2026-02-11' "## 2026-02-11`n`n- Teams-Problem HKR: Steffen bestellt Netzwerkkarten`n- Angebot DL fuer AM: Jenni umgezogen`n- Ticket Ossenbuehl Citrix: alter IE, warten auf neue Farm`n- TISAX Situationen simulieren" | Out-Null
Add-HistoryEntry $journalId '2026-02-03' "## 2026-02-03`n`n- Pen-Test vor TISAX (ueber RES)`n- Steffen: neue Hardware (PFSense nicht auf alter Sophos)`n- TISAX Produktionsrechner: VLAN, PC-0259`n- Doku Backup-Prozess noch nicht gestartet`n- DocuSnap Termin" | Out-Null
Add-HistoryEntry $journalId '2026-01-28' "## 2026-01-28`n`n- viflow in Arbeit`n- Notebook Karsten: Rücksprache ML/TG`n- Focus TISAX Doku SteFi`n- Azure Mails an IT-Team / Helpdesk`n- ShutDown Stromverteiler Sa wieder anlaufen`n- B+W Server Aufstellung (EOL, Greenbone, Risiko)" | Out-Null
Add-HistoryEntry $journalId '2026-01-13' "## 2026-01-13`n`n- Hees in Migration Domaene einbinden`n- Backup-Aufgaben in Plane`n- USV-Analyse mit Hees starten`n- Admin-Rechte HKR abschalten`n- Wildcard-Zertifikat ueberall?`n- NAC: Liste Geraete vorbereiten" | Out-Null
Add-HistoryEntry $journalId '2026-01-07' "## 2026-01-07`n`n- PC QS: Ticket #393176`n- USB-Sticks sperren HKR (Start KW2 2026)`n- Status Festplattenbestellung IBM?`n- Backlogs AtWork bearbeiten" | Out-Null
Add-HistoryEntry $journalId '2025-12-11' "## 2025-12-11`n`n- Access to Snipe-IT lost`n- Qlik: Sales + FI, Florian + Steffen einbinden`n- Teleport: Problem Setup`n- KPI Database: Phase 1 Qlik Ersatz, VIZLIB Plugin Test`n- Keine Admin-Rechte mehr fuer KAS" | Out-Null
Add-HistoryEntry $journalId '2025-12-10' "## 2025-12-10`n`n- KI-Workshop IT-Team: Eray, KI-Governance Philip`n- DC-Problem 2025: warten auf SVA Statement`n- TISAX 5.3.2 Netzwerkdienste`n- Admin-Rechte HKR entfernen (lokale Gruppe NW-Konfig)`n- Reparatur-Prozess: Aufkleber Ticketnummer" | Out-Null
Add-HistoryEntry $journalId '2025-11-25' "## 2025-11-25`n`n- Neue viFlow Server einrichten`n- USB-Sticks sperren ab 01. Dez`n- Server kuenftig nur auf Englisch`n- Findings HKR: XP fluten Netz, Sky GO`n- Bestellung Kelobit DocuSnap Server" | Out-Null
Add-HistoryEntry $journalId '2025-11-19' "## 2025-11-19`n`n- Snipe-IT fuer alle (Ausgabe-, Wareneingangs-, Nachbestellprozess)`n- Netzwerkdokumentation aktualisieren`n- Ordnung in Teleport`n- Verrechnung IT-Kosten (SAN, Teleport + GIT -> KST Server)" | Out-Null
Add-HistoryEntry $journalId '2025-11-12' "## 2025-11-12`n`n- WWP Bollenbach: NiceLabel -> Printer3`n- Win10 PCs HKR: Steffen banft`n- WITEC-Telefone: H+Ue kontaktiert Hersteller" | Out-Null
Add-HistoryEntry $journalId '2025-11-05' "## 2025-11-05`n`n- Teams: aktuell laeuft es`n- HKR: SIP-Trunk 08. Dez Thalheim`n- BitSight Treffen`n- Visitor process: Android-Tool`n- Praktikanten Bewerbungen" | Out-Null
Add-HistoryEntry $journalId '2025-10-28' "## 2025-10-28`n`n- Telekom Hardware waehrend Urlaub?`n- Server AccessManager`n- Citrix: Start Testphase?`n- WITEC final zurueckgebaut?`n- Abschaltung altes Intranet Jahresende`n- Angebote Hardware Bechtle vs. Hees" | Out-Null
# ── 6. Upload ─────────────────────────────────────────────────────────────────
Write-Host "`n[6] Uploading..." -ForegroundColor Cyan
$ulArgs = @('-File', "$scripts\upload.ps1")
if ($Prod) { $ulArgs += '-Prod' }
if ($Force) { $ulArgs += '-Force' }
& powershell @ulArgs
if ($LASTEXITCODE -ne 0) { throw "Upload failed (exit $LASTEXITCODE)" }
Write-Host "`n[DONE] JF Sysadmins import complete." -ForegroundColor Green

View File

@ -71,9 +71,12 @@ if (-not $LockToken) {
if ($Prod) {
$BaseUrl = "https://ka-note.azurewebsites.net"
$BearerToken = if ($Token) { $Token } else { $env:KA_NOTE_TOKEN }
$BearerToken = if ($Token) { $Token } elseif ($env:KA_NOTE_TOKEN) { $env:KA_NOTE_TOKEN } else {
Write-Host " Acquiring token via MSAL..." -ForegroundColor DarkGray
& "$PSScriptRoot\get-token.ps1"
}
if (-not $BearerToken) {
Write-Err "Prod mode requires a Bearer token. Pass -Token or set `$env:KA_NOTE_TOKEN."
Write-Err "Failed to acquire a Bearer token."
exit 1
}
$Headers = @{ Authorization = "Bearer $BearerToken"; "Content-Type" = "application/json" }

View File

@ -65,9 +65,12 @@ if (-not $LockToken) {
if ($Prod) {
$BaseUrl = "https://ka-note.azurewebsites.net"
$BearerToken = if ($Token) { $Token } else { $env:KA_NOTE_TOKEN }
$BearerToken = if ($Token) { $Token } elseif ($env:KA_NOTE_TOKEN) { $env:KA_NOTE_TOKEN } else {
Write-Host " Acquiring token via MSAL..." -ForegroundColor DarkGray
& "$PSScriptRoot\get-token.ps1"
}
if (-not $BearerToken) {
Write-Err "Prod mode requires a Bearer token. Pass -Token or set `$env:KA_NOTE_TOKEN."
Write-Err "Failed to acquire a Bearer token."
exit 1
}
$Headers = @{ Authorization = "Bearer $BearerToken"; "Content-Type" = "application/octet-stream" }
@ -89,7 +92,23 @@ Write-Host ""
Write-Host " Compressing work/..." -ForegroundColor DarkGray
if (Test-Path $ZipPath) { Remove-Item $ZipPath -Force }
Compress-Archive -Path "$WorkDir\*" -DestinationPath $ZipPath
# Use .NET ZipArchive directly to ensure forward-slash entry names (Compress-Archive uses backslashes on Windows)
Add-Type -Assembly 'System.IO.Compression'
Add-Type -Assembly 'System.IO.Compression.FileSystem'
$zipStream = [System.IO.File]::Open($ZipPath, [System.IO.FileMode]::Create)
$archive = [System.IO.Compression.ZipArchive]::new($zipStream, [System.IO.Compression.ZipArchiveMode]::Create)
Get-ChildItem -Path $WorkDir -Recurse -File | Where-Object { $_.FullName -ne $ZipPath } | ForEach-Object {
$entryName = $_.FullName.Substring($WorkDir.Length + 1).Replace('\', '/')
$entry = $archive.CreateEntry($entryName, [System.IO.Compression.CompressionLevel]::Optimal)
$entryStream = $entry.Open()
$fileStream = [System.IO.File]::OpenRead($_.FullName)
$fileStream.CopyTo($entryStream)
$fileStream.Dispose()
$entryStream.Dispose()
}
$archive.Dispose()
$zipStream.Dispose()
$ZipSizeKb = [math]::Round((Get-Item $ZipPath).Length / 1KB, 1)
Write-KV "zip size" "${ZipSizeKb} KB"