Add proper login to Admin UI, replace Caddy basic_auth
All checks were successful
Deploy to Server / deploy (push) Successful in 1m55s

- iron-session for encrypted httpOnly session cookies
- POST /api/auth/login: verifies ADMIN_USERNAME/ADMIN_PASSWORD, sets session, returns API key
- POST /api/auth/logout: destroys session
- middleware.ts: protects all routes, redirects to /login if unauthenticated
- Login page: username + password form (no more browser popup)
- Sidebar: logout calls API route and clears session
- docker-compose.prod.yml: admin-ui reads /opt/rustycms/.env.admin
- deploy.yml: generates .env.admin from Gitea secrets
- Caddy: basic_auth removed from /admin* block

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Peter Meier
2026-03-15 21:49:32 +01:00
parent c644c08222
commit b432621919
13 changed files with 303 additions and 118 deletions

View File

@@ -1,10 +1,14 @@
{
"LoginPage": {
"title": "Anmelden",
"apiKeyLabel": "API-Schlüssel",
"apiKeyPlaceholder": "API-Schlüssel eingeben",
"title": "Anmelden bei RustyCMS",
"usernameLabel": "Benutzername",
"usernamePlaceholder": "Benutzername",
"passwordLabel": "Passwort",
"passwordPlaceholder": "Passwort",
"submit": "Anmelden",
"hint": "Gleicher Schlüssel wie RUSTYCMS_API_KEY auf dem Server. Ohne Schlüssel nur Lesen, mit Schlüssel Bearbeiten."
"loggingIn": "Anmelden…",
"invalidCredentials": "Ungültige Zugangsdaten",
"networkError": "Netzwerkfehler, bitte erneut versuchen"
},
"Sidebar": {
"dashboard": "Dashboard",
@@ -52,39 +56,39 @@
"noAssets": "Noch keine Assets. Lade Bilder im Bereich Assets hoch.",
"status": "Status",
"statusDraft": "Entwurf",
"statusPublished": "Ver\u00f6ffentlicht",
"statusHint": "Entw\u00fcrfe sind \u00fcber die \u00f6ffentliche API nicht sichtbar."
"statusPublished": "Veröffentlicht",
"statusHint": "Entwürfe sind über die öffentliche API nicht sichtbar."
},
"SearchableSelect": {
"placeholder": "\u2014 Bitte ausw\u00e4hlen \u2014",
"clearLabel": "\u2014 Auswahl aufheben \u2014",
"filterPlaceholder": "Filtern\u2026",
"placeholder": " Bitte auswählen —",
"clearLabel": " Auswahl aufheben ",
"filterPlaceholder": "Filtern",
"emptyLabel": "Keine Treffer"
},
"ReferenceField": {
"typeLabel": "Typ: {collection}",
"typesLabel": "Typen: {collections}",
"selectType": "\u2014 Typ w\u00e4hlen \u2014",
"selectType": "— Typ wählen —",
"newEntry": "Neuer Eintrag",
"openEntry": "Eintrag \u00f6ffnen",
"openEntry": "Eintrag öffnen",
"noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu."
},
"ReferenceArrayField": {
"typeLabel": "Typ: {collection}",
"typesLabel": "Typen: {collections}",
"componentType": "Komponententyp",
"selectType": "\u2014 Typ w\u00e4hlen \u2014",
"selectFromExisting": "\u2014 Aus vorhandenen w\u00e4hlen \u2014",
"filterPlaceholder": "Filtern\u2026",
"selectType": "— Typ wählen —",
"selectFromExisting": " Aus vorhandenen wählen —",
"filterPlaceholder": "Filtern",
"emptyLabel": "Keine Treffer",
"selectExistingAriaLabel": "Vorhandenen Eintrag zum Hinzuf\u00fcgen ausw\u00e4hlen",
"selectExistingAriaLabel": "Vorhandenen Eintrag zum Hinzufügen auswählen",
"moveUp": "Nach oben",
"moveDown": "Nach unten",
"remove": "Entfernen",
"newComponent": "Neue {collection}-Komponente",
"createNewComponent": "+ Neue Komponente erstellen\u2026",
"openInNewTab": "In neuem Tab \u00f6ffnen; dann Seite neu laden.",
"openEntry": "Eintrag \u00f6ffnen",
"createNewComponent": "+ Neue Komponente erstellen",
"openInNewTab": "In neuem Tab öffnen; dann Seite neu laden.",
"openEntry": "Eintrag öffnen",
"noCollection": "Keine Referenz-Collection im Schema. Setze {collectionCode} oder {collectionsCode} im Typ, oder starte die API und lade die Seite neu."
},
"MarkdownEditor": {
@@ -92,21 +96,21 @@
"italic": "Kursiv",
"code": "Code",
"link": "Link",
"bulletList": "Aufz\u00e4hlungsliste",
"bulletListButton": "\u2022 Liste",
"placeholder": "Markdown eingeben\u2026 **fett**, *kursiv*, [Link](url), - Liste",
"bulletList": "Aufzählungsliste",
"bulletListButton": " Liste",
"placeholder": "Markdown eingeben **fett**, *kursiv*, [Link](url), - Liste",
"preview": "Vorschau",
"emptyPreview": "Leer \u2014 Vorschau erscheint beim Tippen."
"emptyPreview": "Leer Vorschau erscheint beim Tippen."
},
"PaginationLinks": {
"back": "Zur\u00fcck",
"back": "Zurück",
"next": "Weiter",
"pageInfo": "Seite {page} von {totalPages} ({total} Eintr\u00e4ge)"
"pageInfo": "Seite {page} von {totalPages} ({total} Einträge)"
},
"DataPreviewPanel": {
"hide": "Daten-Vorschau ausblenden",
"show": "Daten-Vorschau",
"loading": "Laden\u2026",
"loading": "Laden",
"errorLoading": "Fehler beim Laden"
},
"SchemaPanel": {
@@ -125,14 +129,14 @@
"sectionSchema": "Schema",
"sectionDataPreview": "Aktuelle Daten",
"copyCode": "Code kopieren",
"loading": "Laden\u2026",
"loading": "Laden",
"errorLoading": "Fehler beim Laden"
},
"ReferenceOrInlineField": {
"reference": "Referenz",
"inline": "Eingebettet",
"inlineObject": "Eingebettetes Objekt (keine Referenz)",
"noInlineSchema": "Kein Inline-Schema. Seite neu laden oder API pr\u00fcfen (useFields / collection)."
"noInlineSchema": "Kein Inline-Schema. Seite neu laden oder API prüfen (useFields / collection)."
},
"LocaleSwitcher": {
"label": "Sprache"
@@ -170,38 +174,38 @@
},
"Dashboard": {
"title": "Dashboard",
"subtitle": "W\u00e4hle eine Sammlung zur Inhaltsverwaltung.",
"subtitle": "Wähle eine Sammlung zur Inhaltsverwaltung.",
"newContentType": "Neuer Inhaltstyp",
"searchPlaceholder": "Inhaltstypen suchen…",
"filterByTag": "Tag:",
"tagAll": "Alle",
"noResults": "Kein Inhaltstyp entspricht Suche oder Filter.",
"noCollections": "Keine Sammlungen geladen. Pr\u00fcfe ob die RustyCMS-API unter {url} erreichbar ist.",
"recentSectionTitle": "Letzte 3 bearbeitete Beitr\u00e4ge",
"recentSectionLink": "Alle Beitr\u00e4ge",
"recentSectionEmpty": "Noch keine Beitr\u00e4ge.",
"noCollections": "Keine Sammlungen geladen. Prüfe ob die RustyCMS-API unter {url} erreichbar ist.",
"recentSectionTitle": "Letzte 3 bearbeitete Beiträge",
"recentSectionLink": "Alle Beiträge",
"recentSectionEmpty": "Noch keine Beiträge.",
"loading": "Laden…"
},
"TypesPage": {
"title": "Typen",
"newType": "Neuer Typ",
"description": "Inhaltstypen (Sammlungen). Schema bearbeiten oder Typ l\u00f6schen. Beim L\u00f6schen wird nur die Typdefinitionsdatei entfernt; vorhandene Inhaltseintr\u00e4ge bleiben erhalten.",
"description": "Inhaltstypen (Sammlungen). Schema bearbeiten oder Typ löschen. Beim Löschen wird nur die Typdefinitionsdatei entfernt; vorhandene Inhaltseinträge bleiben erhalten.",
"searchPlaceholder": "Inhaltstypen suchen…",
"filterByTag": "Tag:",
"tagAll": "Alle",
"noResults": "Keine Typen entsprechen Ihrer Suche oder dem Filter.",
"loading": "Laden\u2026",
"loading": "Laden",
"errorLoading": "Fehler beim Laden der Typen: {error}",
"noTypes": "Noch keine Typen vorhanden. Erstelle einen mit \"Neuer Typ\".",
"colName": "Name",
"colDescription": "Beschreibung",
"colCategory": "Kategorie",
"colActions": "Aktionen",
"confirmDelete": "\"{name}\" l\u00f6schen?",
"confirmDeleteFinal": "\"{name}\" wirklich l\u00f6schen? Dies kann nicht r\u00fcckg\u00e4ngig gemacht werden.",
"delete": "L\u00f6schen",
"yesDelete": "Ja, l\u00f6schen",
"deleting": "\u2026",
"confirmDelete": "\"{name}\" löschen?",
"confirmDeleteFinal": "\"{name}\" wirklich löschen? Dies kann nicht rückgängig gemacht werden.",
"delete": "Löschen",
"yesDelete": "Ja, löschen",
"deleting": "",
"cancel": "Abbrechen",
"edit": "Bearbeiten"
},
@@ -211,41 +215,41 @@
"nameRequired": "Name ist erforderlich.",
"nameInvalid": "Name: nur Kleinbuchstaben, Ziffern und Unterstriche.",
"fieldRequired": "Mindestens ein Feld erforderlich.",
"fieldNamesUnique": "Feldnamen m\u00fcssen eindeutig sein.",
"fieldNamesUnique": "Feldnamen müssen eindeutig sein.",
"errorCreating": "Fehler beim Erstellen des Typs.",
"nameLabel": "Name",
"namePlaceholder": "z.\u00a0B. produkt, blogbeitrag",
"namePlaceholder": "z. B. produkt, blogbeitrag",
"nameHint": "Nur Kleinbuchstaben, Ziffern und Unterstriche.",
"descriptionLabel": "Beschreibung",
"categoryLabel": "Kategorie",
"categoryPlaceholder": "z.\u00a0B. inhalt",
"categoryPlaceholder": "z. B. inhalt",
"tagsLabel": "Tags (kommagetrennt)",
"tagsPlaceholder": "z.\u00a0B. inhalt, blog",
"tagsPlaceholder": "z. B. inhalt, blog",
"strictLabel": "Strikt (unbekannte Felder ablehnen)",
"fieldsLabel": "Felder",
"addField": "Feld hinzuf\u00fcgen",
"addField": "Feld hinzufügen",
"fieldNamePlaceholder": "Feldname",
"fieldTypeLabel": "Feldtyp",
"required": "Pflichtfeld",
"removeField": "Feld entfernen",
"collectionPlaceholder": "Sammlung (z.\u00a0B. seite)",
"collectionPlaceholder": "Sammlung (z. B. seite)",
"allowedSlugsPlaceholder": "Erlaubte Slugs (kommagetrennt, optional)",
"allowedCollectionsPlaceholder": "Erlaubte Inhaltstypen (kommagetrennt, optional)",
"arrayItemType": "Array-Elementtyp",
"itemTypePlaceholder": "z.\u00a0B. string, reference",
"arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—w\u00e4hle unten, was ein Element ist.",
"itemTypePlaceholder": "z. B. string, reference",
"arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—wähle unten, was ein Element ist.",
"arrayEachEntry": "Ein Listeneintrag ist",
"itemKindString": "Einfacher Text (string)",
"itemKindNumber": "Zahl (number)",
"itemKindObject": "Objekt (feste Unterfelder pro Eintrag)",
"itemKindReference": "Referenz (Slug auf einen Eintrag einer Sammlung)",
"objectItemFieldsLabel": "Felder pro Listeneintrag",
"addObjectField": "Unterfeld hinzuf\u00fcgen",
"addObjectField": "Unterfeld hinzufügen",
"objectFieldNamePlaceholder": "Unterfeldname",
"arrayReferenceHelp": "Alle Slugs in der Liste m\u00fcssen in dieser Sammlung existieren (oder in der Whitelist stehen).",
"arrayReferenceHelp": "Alle Slugs in der Liste müssen in dieser Sammlung existieren (oder in der Whitelist stehen).",
"multiSelectOptions": "Optionen",
"multiSelectOptionsPlaceholder": "z.\u00a0B. option1, option2, option3",
"multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es k\u00f6nnen mehrere ausgew\u00e4hlt werden.",
"multiSelectOptionsPlaceholder": "z. B. option1, option2, option3",
"multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es können mehrere ausgewählt werden.",
"stringWidgetLabel": "Eingabeart",
"stringWidgetSingleline": "Einzeilig",
"stringWidgetTextarea": "Mehrzeilig (Textbereich)",
@@ -257,34 +261,34 @@
"codeLanguageJson": "JSON",
"codeLanguageHtml": "HTML",
"defaultValueLabel": "Standardwert",
"defaultValuePlaceholder": "z.\u00a0B. \"text\", 0, true, [\"a\",\"b\"]",
"defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Eintr\u00e4gen verwendet.",
"defaultValueInvalid": "Ung\u00fcltiges JSON f\u00fcr Standardwert im Feld \"{field}\"",
"defaultValuePlaceholder": "z. B. \"text\", 0, true, [\"a\",\"b\"]",
"defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Einträgen verwendet.",
"defaultValueInvalid": "Ungültiges JSON für Standardwert im Feld \"{field}\"",
"defaultValueBoolean": "Standard: angehakt",
"defaultValueEmpty": "Leer lassen = keiner",
"defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard ausw\u00e4hlen.",
"defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard auswählen.",
"defaultValueArrayPlaceholder": "Kommagetrennte Werte",
"fieldDescriptionPlaceholder": "Feldbeschreibung (optional)",
"creating": "Erstellen\u2026",
"creating": "Erstellen",
"createType": "Typ erstellen",
"cancel": "Abbrechen"
},
"EditTypePage": {
"fieldRequired": "Mindestens ein Feld erforderlich.",
"fieldNamesUnique": "Feldnamen m\u00fcssen eindeutig sein.",
"fieldNamesUnique": "Feldnamen müssen eindeutig sein.",
"errorSaving": "Fehler beim Speichern des Typs.",
"missingName": "Typname fehlt.",
"backToTypes": "Zur\u00fcck zu Typen",
"loading": "Laden\u2026",
"backToTypes": "Zurück zu Typen",
"loading": "Laden",
"errorLoading": "Fehler beim Laden des Typs: {error}",
"title": "Typ bearbeiten: {name}",
"description": "Beschreibung, Kategorie, Tags und Felder \u00e4ndern. Die Schemadatei wird auf dem Server aktualisiert.",
"description": "Beschreibung, Kategorie, Tags und Felder ändern. Die Schemadatei wird auf dem Server aktualisiert.",
"nameLabel": "Name",
"descriptionLabel": "Beschreibung",
"categoryLabel": "Kategorie",
"categoryPlaceholder": "z.\u00a0B. inhalt",
"categoryPlaceholder": "z. B. inhalt",
"tagsLabel": "Tags (kommagetrennt)",
"tagsPlaceholder": "z.\u00a0B. inhalt, blog",
"tagsPlaceholder": "z. B. inhalt, blog",
"strictLabel": "Strikt (unbekannte Felder ablehnen)",
"extendsLabel": "Erweitert",
"extendsDescription": "Dieser Typ erbt Felder von diesen Typen. Gehe zum jeweiligen Typ, um geerbte Felder zu bearbeiten.",
@@ -293,7 +297,7 @@
"addExtend": "Hinzufügen",
"removeExtend": "Aus Erweiterungen entfernen",
"fieldsLabel": "Felder",
"addField": "Feld hinzuf\u00fcgen",
"addField": "Feld hinzufügen",
"moveFieldUp": "Feld nach oben",
"moveFieldDown": "Feld nach unten",
"moveFieldToTop": "Ganz nach oben",
@@ -302,7 +306,7 @@
"fieldTypeLabel": "Feldtyp",
"required": "Pflichtfeld",
"removeField": "Feld entfernen",
"collectionPlaceholder": "Sammlung (z.\u00a0B. seite)",
"collectionPlaceholder": "Sammlung (z. B. seite)",
"allowedSlugsPlaceholder": "Erlaubte Slugs (kommagetrennt, optional)",
"allowedCollectionsPlaceholder": "Erlaubte Inhaltstypen (kommagetrennt, optional)",
"patternLabel": "Pattern (Regex)",
@@ -310,20 +314,20 @@
"minLengthLabel": "Min. Länge",
"maxLengthLabel": "Max. Länge",
"arrayItemType": "Array-Elementtyp",
"itemTypePlaceholder": "z.\u00a0B. string, reference",
"arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—w\u00e4hle unten, was ein Element ist.",
"itemTypePlaceholder": "z. B. string, reference",
"arrayExplain": "Dieses Feld ist in JSON eine Liste [ ]. Jeder Eintrag hat denselben Typ—wähle unten, was ein Element ist.",
"arrayEachEntry": "Ein Listeneintrag ist",
"itemKindString": "Einfacher Text (string)",
"itemKindNumber": "Zahl (number)",
"itemKindObject": "Objekt (feste Unterfelder pro Eintrag)",
"itemKindReference": "Referenz (Slug auf einen Eintrag einer Sammlung)",
"objectItemFieldsLabel": "Felder pro Listeneintrag",
"addObjectField": "Unterfeld hinzuf\u00fcgen",
"addObjectField": "Unterfeld hinzufügen",
"objectFieldNamePlaceholder": "Unterfeldname",
"arrayReferenceHelp": "Alle Slugs in der Liste m\u00fcssen in dieser Sammlung existieren (oder in der Whitelist stehen).",
"arrayReferenceHelp": "Alle Slugs in der Liste müssen in dieser Sammlung existieren (oder in der Whitelist stehen).",
"multiSelectOptions": "Optionen",
"multiSelectOptionsPlaceholder": "z.\u00a0B. option1, option2, option3",
"multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es k\u00f6nnen mehrere ausgew\u00e4hlt werden.",
"multiSelectOptionsPlaceholder": "z. B. option1, option2, option3",
"multiSelectOptionsHelp": "Komma- oder zeilengetrennte Liste erlaubter Werte. Es können mehrere ausgewählt werden.",
"stringWidgetLabel": "Eingabeart",
"stringWidgetSingleline": "Einzeilig",
"stringWidgetTextarea": "Mehrzeilig (Textbereich)",
@@ -335,15 +339,15 @@
"codeLanguageJson": "JSON",
"codeLanguageHtml": "HTML",
"defaultValueLabel": "Standardwert",
"defaultValuePlaceholder": "z.\u00a0B. \"text\", 0, true, [\"a\",\"b\"]",
"defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Eintr\u00e4gen verwendet.",
"defaultValueInvalid": "Ung\u00fcltiges JSON f\u00fcr Standardwert im Feld \"{field}\"",
"defaultValuePlaceholder": "z. B. \"text\", 0, true, [\"a\",\"b\"]",
"defaultValueHelp": "JSON-Wert; leer lassen = keiner. Wird bei neuen Einträgen verwendet.",
"defaultValueInvalid": "Ungültiges JSON für Standardwert im Feld \"{field}\"",
"defaultValueBoolean": "Standard: angehakt",
"defaultValueEmpty": "Leer lassen = keiner",
"defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard ausw\u00e4hlen.",
"defaultValueMultiSelectSetOptions": "Zuerst Optionen oben eintragen, dann Standard auswählen.",
"defaultValueArrayPlaceholder": "Kommagetrennte Werte",
"fieldDescriptionPlaceholder": "Feldbeschreibung (optional)",
"saving": "Speichern\u2026",
"saving": "Speichern",
"save": "Speichern",
"cancel": "Abbrechen"
},
@@ -446,11 +450,11 @@
"transformPresetMedium": "Mittel 800px",
"transformPresetJpeg": "JPEG 1200px",
"transformWidth": "Breite",
"transformHeight": "H\u00f6he",
"transformAspect": "Seitenverh\u00e4ltnis",
"transformHeight": "Höhe",
"transformAspect": "Seitenverhältnis",
"transformFit": "Fit",
"transformFormat": "Format",
"transformQuality": "Qualit\u00e4t (1100)",
"transformQuality": "Qualität (1100)",
"creating": "Wird erstellt…"
}
}
}