v2.4.0 — OIDC login, OpenStreetMap search, account management

Features:
- Single Sign-On (OIDC) — login with Google, Apple, Authentik, Keycloak
- OpenStreetMap place search as free fallback when no Google API key
- Change password in user settings
- Delete own account (with last-admin protection)
- Last login column in admin user management
- SSO badge and provider info in user settings
- Google API key "Recommended" badge in admin panel

Improvements:
- API keys load correctly after page reload
- Validate auto-saves keys before testing
- Time format respects 12h/24h setting everywhere
- Dark mode fixes for popups and backup buttons
- Admin stats: removed photos, 4-column layout
- Profile picture upload button on avatar overlay
- TravelStats duplicate key fix
- Backup panel dark mode support
This commit is contained in:
Maurice
2026-03-19 23:49:07 +01:00
parent 74be63555d
commit c887acddee
21 changed files with 779 additions and 97 deletions
+5 -5
View File
@@ -191,7 +191,7 @@ export default function BackupPanel() {
<button
onClick={handleCreate}
disabled={isCreating}
className="flex items-center gap-2 bg-slate-700 text-white px-4 py-2 rounded-lg hover:bg-slate-900 text-sm font-medium disabled:opacity-60"
className="flex items-center gap-2 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 px-4 py-2 rounded-lg hover:bg-slate-900 text-sm font-medium disabled:opacity-60"
>
{isCreating ? (
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
@@ -289,7 +289,7 @@ export default function BackupPanel() {
</div>
<button
onClick={() => handleAutoSettingsChange('enabled', !autoSettings.enabled)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${autoSettings.enabled ? 'bg-slate-700' : 'bg-gray-200'}`}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${autoSettings.enabled ? 'bg-slate-900 dark:bg-slate-100' : 'bg-gray-200 dark:bg-gray-600'}`}
>
<span className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${autoSettings.enabled ? 'translate-x-6' : 'translate-x-1'}`} />
</button>
@@ -307,7 +307,7 @@ export default function BackupPanel() {
onClick={() => handleAutoSettingsChange('interval', opt.value)}
className={`px-4 py-2 rounded-lg text-sm font-medium border transition-colors ${
autoSettings.interval === opt.value
? 'bg-slate-700 text-white border-slate-700'
? 'bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 border-slate-700'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'
}`}
>
@@ -327,7 +327,7 @@ export default function BackupPanel() {
onClick={() => handleAutoSettingsChange('keep_days', opt.value)}
className={`px-4 py-2 rounded-lg text-sm font-medium border transition-colors ${
autoSettings.keep_days === opt.value
? 'bg-slate-700 text-white border-slate-700'
? 'bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 border-slate-700'
: 'bg-white text-gray-600 border-gray-200 hover:border-gray-300'
}`}
>
@@ -344,7 +344,7 @@ export default function BackupPanel() {
<button
onClick={handleSaveAutoSettings}
disabled={autoSettingsSaving || !autoSettingsDirty}
className="flex items-center gap-2 bg-slate-700 text-white px-5 py-2 rounded-lg hover:bg-slate-900 text-sm font-medium disabled:opacity-50 transition-colors"
className="flex items-center gap-2 bg-slate-900 dark:bg-slate-100 text-white dark:text-slate-900 px-5 py-2 rounded-lg hover:bg-slate-900 text-sm font-medium disabled:opacity-50 transition-colors"
>
{autoSettingsSaving
? <div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
@@ -5,7 +5,7 @@ import { useTranslation } from '../../i18n'
import { useSettingsStore } from '../../store/settingsStore'
// Numeric ISO → country name lookup (countries-110m uses numeric IDs)
const NUMERIC_TO_NAME = {"004":"Afghanistan","008":"Albania","012":"Algeria","024":"Angola","032":"Argentina","036":"Australia","040":"Austria","050":"Bangladesh","056":"Belgium","064":"Bhutan","068":"Bolivia","070":"Bosnia and Herzegovina","072":"Botswana","076":"Brazil","100":"Bulgaria","104":"Myanmar","108":"Burundi","112":"Belarus","116":"Cambodia","120":"Cameroon","124":"Canada","140":"Central African Republic","144":"Sri Lanka","148":"Chad","152":"Chile","156":"China","170":"Colombia","178":"Congo","180":"Democratic Republic of the Congo","188":"Costa Rica","191":"Croatia","192":"Cuba","196":"Cyprus","203":"Czech Republic","204":"Benin","208":"Denmark","214":"Dominican Republic","218":"Ecuador","818":"Egypt","222":"El Salvador","226":"Equatorial Guinea","232":"Eritrea","233":"Estonia","231":"Ethiopia","238":"Falkland Islands","246":"Finland","250":"France","266":"Gabon","270":"Gambia","268":"Georgia","276":"Germany","288":"Ghana","300":"Greece","320":"Guatemala","324":"Guinea","328":"Guyana","332":"Haiti","340":"Honduras","348":"Hungary","352":"Iceland","356":"India","360":"Indonesia","364":"Iran","368":"Iraq","372":"Ireland","376":"Israel","380":"Italy","384":"Ivory Coast","388":"Jamaica","392":"Japan","400":"Jordan","398":"Kazakhstan","404":"Kenya","408":"North Korea","410":"South Korea","414":"Kuwait","417":"Kyrgyzstan","418":"Laos","422":"Lebanon","426":"Lesotho","430":"Liberia","434":"Libya","440":"Lithuania","442":"Luxembourg","450":"Madagascar","454":"Malawi","458":"Malaysia","466":"Mali","478":"Mauritania","484":"Mexico","496":"Mongolia","498":"Moldova","504":"Morocco","508":"Mozambique","516":"Namibia","524":"Nepal","528":"Netherlands","540":"New Caledonia","554":"New Zealand","558":"Nicaragua","562":"Niger","566":"Nigeria","578":"Norway","512":"Oman","586":"Pakistan","591":"Panama","598":"Papua New Guinea","600":"Paraguay","604":"Peru","608":"Philippines","616":"Poland","620":"Portugal","630":"Puerto Rico","634":"Qatar","642":"Romania","643":"Russia","646":"Rwanda","682":"Saudi Arabia","686":"Senegal","688":"Serbia","694":"Sierra Leone","703":"Slovakia","705":"Slovenia","706":"Somalia","710":"South Africa","724":"Spain","144":"Sri Lanka","729":"Sudan","740":"Suriname","748":"Swaziland","752":"Sweden","756":"Switzerland","760":"Syria","762":"Tajikistan","764":"Thailand","768":"Togo","780":"Trinidad and Tobago","788":"Tunisia","792":"Turkey","795":"Turkmenistan","800":"Uganda","804":"Ukraine","784":"United Arab Emirates","826":"United Kingdom","840":"United States of America","858":"Uruguay","860":"Uzbekistan","862":"Venezuela","704":"Vietnam","887":"Yemen","894":"Zambia","716":"Zimbabwe"}
const NUMERIC_TO_NAME = {"004":"Afghanistan","008":"Albania","012":"Algeria","024":"Angola","032":"Argentina","036":"Australia","040":"Austria","050":"Bangladesh","056":"Belgium","064":"Bhutan","068":"Bolivia","070":"Bosnia and Herzegovina","072":"Botswana","076":"Brazil","100":"Bulgaria","104":"Myanmar","108":"Burundi","112":"Belarus","116":"Cambodia","120":"Cameroon","124":"Canada","140":"Central African Republic","144":"Sri Lanka","148":"Chad","152":"Chile","156":"China","170":"Colombia","178":"Congo","180":"Democratic Republic of the Congo","188":"Costa Rica","191":"Croatia","192":"Cuba","196":"Cyprus","203":"Czech Republic","204":"Benin","208":"Denmark","214":"Dominican Republic","218":"Ecuador","818":"Egypt","222":"El Salvador","226":"Equatorial Guinea","232":"Eritrea","233":"Estonia","231":"Ethiopia","238":"Falkland Islands","246":"Finland","250":"France","266":"Gabon","270":"Gambia","268":"Georgia","276":"Germany","288":"Ghana","300":"Greece","320":"Guatemala","324":"Guinea","328":"Guyana","332":"Haiti","340":"Honduras","348":"Hungary","352":"Iceland","356":"India","360":"Indonesia","364":"Iran","368":"Iraq","372":"Ireland","376":"Israel","380":"Italy","384":"Ivory Coast","388":"Jamaica","392":"Japan","400":"Jordan","398":"Kazakhstan","404":"Kenya","408":"North Korea","410":"South Korea","414":"Kuwait","417":"Kyrgyzstan","418":"Laos","422":"Lebanon","426":"Lesotho","430":"Liberia","434":"Libya","440":"Lithuania","442":"Luxembourg","450":"Madagascar","454":"Malawi","458":"Malaysia","466":"Mali","478":"Mauritania","484":"Mexico","496":"Mongolia","498":"Moldova","504":"Morocco","508":"Mozambique","516":"Namibia","524":"Nepal","528":"Netherlands","540":"New Caledonia","554":"New Zealand","558":"Nicaragua","562":"Niger","566":"Nigeria","578":"Norway","512":"Oman","586":"Pakistan","591":"Panama","598":"Papua New Guinea","600":"Paraguay","604":"Peru","608":"Philippines","616":"Poland","620":"Portugal","630":"Puerto Rico","634":"Qatar","642":"Romania","643":"Russia","646":"Rwanda","682":"Saudi Arabia","686":"Senegal","688":"Serbia","694":"Sierra Leone","703":"Slovakia","705":"Slovenia","706":"Somalia","710":"South Africa","724":"Spain","729":"Sudan","740":"Suriname","748":"Swaziland","752":"Sweden","756":"Switzerland","760":"Syria","762":"Tajikistan","764":"Thailand","768":"Togo","780":"Trinidad and Tobago","788":"Tunisia","792":"Turkey","795":"Turkmenistan","800":"Uganda","804":"Ukraine","784":"United Arab Emirates","826":"United Kingdom","840":"United States of America","858":"Uruguay","860":"Uzbekistan","862":"Venezuela","704":"Vietnam","887":"Yemen","894":"Zambia","716":"Zimbabwe"}
// Our country names from addresses → match against GeoJSON names
function isCountryMatch(geoName, visitedCountries) {
@@ -3,6 +3,7 @@ import Modal from '../shared/Modal'
import { mapsApi, tagsApi, categoriesApi } from '../../api/client'
import { useToast } from '../shared/Toast'
import { useAuthStore } from '../../store/authStore'
import { useTranslation } from '../../i18n'
import { Search, Plus, MapPin, Loader } from 'lucide-react'
const STATUSES = [
@@ -23,7 +24,8 @@ export default function PlaceFormModal({
onTagCreated,
}) {
const isEditing = !!place
const { user } = useAuthStore()
const { user, hasMapsKey } = useAuthStore()
const { t } = useTranslation()
const toast = useToast()
const [categories, setCategories] = useState(initialCategories)
@@ -124,14 +126,17 @@ export default function PlaceFormModal({
}
}
const [searchSource, setSearchSource] = useState(null)
const handleMapSearch = async () => {
if (!mapQuery.trim()) return
setMapSearching(true)
try {
const data = await mapsApi.search(mapQuery)
setMapResults(data.places || [])
setSearchSource(data.source || 'google')
} catch (err) {
toast.error(err.response?.data?.error || 'Maps search failed')
toast.error(err.response?.data?.error || t('places.mapsSearchError'))
} finally {
setMapSearching(false)
}
@@ -218,9 +223,13 @@ export default function PlaceFormModal({
</div>
)}
{/* Google Maps search — always visible when API key is set */}
{user?.maps_api_key && (
{/* Place search — Google Maps or OpenStreetMap fallback */}
<div className="bg-slate-50 rounded-xl p-3 border border-slate-200">
{!hasMapsKey && (
<p className="mb-2 text-xs" style={{ color: 'var(--text-faint)' }}>
{t('places.osmActive')}
</p>
)}
<div className="flex gap-2">
<div className="relative flex-1">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
@@ -229,7 +238,7 @@ export default function PlaceFormModal({
value={mapQuery}
onChange={e => setMapQuery(e.target.value)}
onKeyDown={e => e.key === 'Enter' && handleMapSearch()}
placeholder="Google Maps suchen..."
placeholder={t('places.mapsSearchPlaceholder')}
className="w-full pl-8 pr-3 py-2 text-sm border border-slate-200 rounded-lg focus:ring-2 focus:ring-slate-400 focus:border-transparent bg-white"
/>
</div>
@@ -238,7 +247,7 @@ export default function PlaceFormModal({
disabled={mapSearching}
className="px-3 py-2 bg-slate-900 text-white text-sm rounded-lg hover:bg-slate-700 disabled:opacity-50"
>
{mapSearching ? <Loader className="w-4 h-4 animate-spin" /> : 'Suchen'}
{mapSearching ? <Loader className="w-4 h-4 animate-spin" /> : t('common.search')}
</button>
</div>
@@ -263,7 +272,6 @@ export default function PlaceFormModal({
</div>
)}
</div>
)}
{/* Name */}
<div>
@@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
import Modal from '../shared/Modal'
import CustomSelect from '../shared/CustomSelect'
import { mapsApi } from '../../api/client'
import { useAuthStore } from '../../store/authStore'
import { useToast } from '../shared/Toast'
import { Search } from 'lucide-react'
import { useTranslation } from '../../i18n'
@@ -44,6 +45,7 @@ export default function PlaceFormModal({
const [isSaving, setIsSaving] = useState(false)
const toast = useToast()
const { t, language } = useTranslation()
const { hasMapsKey } = useAuthStore()
useEffect(() => {
if (place) {
@@ -139,8 +141,13 @@ export default function PlaceFormModal({
size="lg"
>
<form onSubmit={handleSubmit} className="space-y-4">
{/* Google Maps Search */}
{/* Place Search */}
<div className="bg-slate-50 rounded-xl p-3 border border-slate-200">
{!hasMapsKey && (
<p className="mb-2 text-xs" style={{ color: 'var(--text-faint)' }}>
{t('places.osmActive')}
</p>
)}
<div className="flex gap-2">
<input
type="text"