feat: Google Maps API key management in admin settings
- Backend: GET /settings/maps-key (public) + PATCH /settings/maps (protected) - Admin: sección para guardar/ver estado de la Maps API key - Flutter web carga la key dinámicamente desde el backend Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
39c4301d1b
commit
3e1632e776
@@ -5,7 +5,8 @@ import { api } from '@/lib/api';
|
|||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Save, ExternalLink, Copy, FileText, Shield } from 'lucide-react';
|
import { Input } from '@/components/ui/input';
|
||||||
|
import { Save, ExternalLink, Copy, FileText, Shield, Map, Eye, EyeOff, CheckCircle2, XCircle } from 'lucide-react';
|
||||||
|
|
||||||
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
|
const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000/api/v1';
|
||||||
const PUBLIC_BASE = API_BASE.replace('/api/v1', '');
|
const PUBLIC_BASE = API_BASE.replace('/api/v1', '');
|
||||||
@@ -30,16 +31,43 @@ export default function SettingsPage() {
|
|||||||
const [saving, setSaving] = useState<string | null>(null);
|
const [saving, setSaving] = useState<string | null>(null);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
// Maps key state
|
||||||
|
const [mapsConfigured, setMapsConfigured] = useState(false);
|
||||||
|
const [mapsKey, setMapsKey] = useState('');
|
||||||
|
const [showMapsKey, setShowMapsKey] = useState(false);
|
||||||
|
const [savingMaps, setSavingMaps] = useState(false);
|
||||||
|
|
||||||
const load = useCallback(() => {
|
const load = useCallback(() => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies')
|
Promise.all([
|
||||||
.then((data) => setPolicies({ privacy: data.privacy || '', terms: data.terms || '' }))
|
api.get<{ privacy: string | null; terms: string | null }>('/settings/policies'),
|
||||||
.catch(() => toast.error('Error al cargar políticas'))
|
api.get<{ configured: boolean; api_key: string }>('/settings/maps-key'),
|
||||||
|
])
|
||||||
|
.then(([policiesData, mapsData]) => {
|
||||||
|
setPolicies({ privacy: policiesData.privacy || '', terms: policiesData.terms || '' });
|
||||||
|
setMapsConfigured(mapsData.configured);
|
||||||
|
})
|
||||||
|
.catch(() => toast.error('Error al cargar configuración'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { load(); }, [load]);
|
useEffect(() => { load(); }, [load]);
|
||||||
|
|
||||||
|
const saveMapsKey = async () => {
|
||||||
|
if (!mapsKey.trim()) return toast.error('Ingresa una API Key');
|
||||||
|
setSavingMaps(true);
|
||||||
|
try {
|
||||||
|
await api.patch('/settings/maps', { api_key: mapsKey.trim() });
|
||||||
|
toast.success('Google Maps API Key guardada');
|
||||||
|
setMapsKey('');
|
||||||
|
setMapsConfigured(true);
|
||||||
|
} catch (e: any) {
|
||||||
|
toast.error(e?.message || 'Error al guardar');
|
||||||
|
} finally {
|
||||||
|
setSavingMaps(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const savePolicy = async (key: string) => {
|
const savePolicy = async (key: string) => {
|
||||||
setSaving(key);
|
setSaving(key);
|
||||||
try {
|
try {
|
||||||
@@ -67,6 +95,54 @@ export default function SettingsPage() {
|
|||||||
<p className="text-muted-foreground text-sm mt-1">Gestiona las políticas legales y ajustes globales de ProsApp.</p>
|
<p className="text-muted-foreground text-sm mt-1">Gestiona las políticas legales y ajustes globales de ProsApp.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Google Maps */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2 text-base">
|
||||||
|
<Map size={18} className="text-muted-foreground" />
|
||||||
|
Google Maps API Key
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Requerida para mostrar mapas y detectar la ubicación en la app web y móvil.
|
||||||
|
Habilita <strong>Maps JavaScript API</strong> y <strong>Geocoding API</strong> en Google Cloud Console.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{mapsConfigured ? (
|
||||||
|
<><CheckCircle2 className="text-green-500 h-5 w-5" /><span className="text-sm font-medium">Configurada</span></>
|
||||||
|
) : (
|
||||||
|
<><XCircle className="text-destructive h-5 w-5" /><span className="text-sm font-medium text-destructive">No configurada</span></>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<div className="relative flex-1">
|
||||||
|
<Input
|
||||||
|
type={showMapsKey ? 'text' : 'password'}
|
||||||
|
placeholder="AIzaSy..."
|
||||||
|
value={mapsKey}
|
||||||
|
onChange={(e) => setMapsKey(e.target.value)}
|
||||||
|
className="pr-10 font-mono"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
|
||||||
|
onClick={() => setShowMapsKey(!showMapsKey)}
|
||||||
|
>
|
||||||
|
{showMapsKey ? <EyeOff size={16} /> : <Eye size={16} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<Button onClick={saveMapsKey} disabled={savingMaps || !mapsKey.trim()}>
|
||||||
|
<Save className="mr-1 h-4 w-4" />
|
||||||
|
{savingMaps ? 'Guardando...' : 'Guardar'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
La app web carga la key desde el backend al abrir el mapa. Agrega restricción HTTP en Google Cloud: <code className="bg-muted px-1 rounded">https://app.prosapp.co/*</code>
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
{/* Policies */}
|
{/* Policies */}
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<h2 className="text-lg font-semibold">Documentos legales</h2>
|
<h2 className="text-lg font-semibold">Documentos legales</h2>
|
||||||
|
|||||||
@@ -29,6 +29,19 @@ export class SettingsController {
|
|||||||
return this.settings.updatePolicy(key, body.content);
|
return this.settings.updatePolicy(key, body.content);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Maps key
|
||||||
|
@Get('maps-key')
|
||||||
|
async getMapsKey() {
|
||||||
|
const api_key = await this.settings.getMapsKey();
|
||||||
|
return { configured: !!api_key, api_key: api_key ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Patch('maps')
|
||||||
|
@UseGuards(JwtAuthGuard) @ApiBearerAuth()
|
||||||
|
saveMapsKey(@Body() body: { api_key: string }) {
|
||||||
|
return this.settings.saveMapsKey(body.api_key);
|
||||||
|
}
|
||||||
|
|
||||||
// Public policy pages
|
// Public policy pages
|
||||||
@Get('policy/:key')
|
@Get('policy/:key')
|
||||||
async getPublicPolicy(@Param('key') key: string, @Res() res: Response) {
|
async getPublicPolicy(@Param('key') key: string, @Res() res: Response) {
|
||||||
|
|||||||
@@ -41,4 +41,17 @@ export class SettingsService {
|
|||||||
]);
|
]);
|
||||||
return { privacy, terms };
|
return { privacy, terms };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getMapsKey(): Promise<string | null> {
|
||||||
|
const setting = await this.prisma.settings.findUnique({ where: { key: 'maps_config' } });
|
||||||
|
return (setting?.value as any)?.api_key ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveMapsKey(api_key: string) {
|
||||||
|
return this.prisma.settings.upsert({
|
||||||
|
where: { key: 'maps_config' },
|
||||||
|
create: { key: 'maps_config', value: { api_key } },
|
||||||
|
update: { value: { api_key } },
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user