Add Authelia as centralized authentication and OIDC provider
Authelia runs as a managed native service on Wild Central, providing two integration patterns for network services: - Forward-auth via HAProxy for apps without native SSO (Lua auth-request script intercepts requests, redirects unauthenticated users to login portal) - OIDC provider for apps with native support (Gitea, Grafana, etc.) Backend: new internal/authelia/ package with service manager, config generation, file-based user management (argon2id), and OIDC client management. API endpoints for status, config, users CRUD, and OIDC clients CRUD. HAProxy: config generator extended with lua-load, auth-request directives, and Authelia backend. Auth directives scoped to session cookie domain — only subdomains of the auth portal's parent are eligible for forward-auth. Frontend: Authentication page with enable/disable, user management, OIDC client management (add/edit/delete), and protected domains toggles. Advanced subsystem page for raw config view. Dashboard service card and sidebar entries.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { NavLink } from 'react-router';
|
||||
import { Sun, Moon, Monitor, Shield, Lock, ShieldAlert, Wifi, LayoutDashboard, Globe, Network, ChevronRight } from 'lucide-react';
|
||||
import { Sun, Moon, Monitor, Shield, Lock, ShieldAlert, ShieldBan, Wifi, LayoutDashboard, Globe, Network, ChevronRight, KeyRound } from 'lucide-react';
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/collapsible';
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -89,10 +89,12 @@ export function AppSidebar() {
|
||||
];
|
||||
|
||||
const centralItems = [
|
||||
{ to: '/auth', icon: KeyRound, label: 'Authentication', daemon: 'authelia' as const },
|
||||
{ to: '/vpn', icon: Lock, label: 'VPN', daemon: 'wireguard' as const },
|
||||
{ to: '/firewall', icon: Shield, label: 'Firewall', daemon: 'nftables' as const },
|
||||
{ to: '/crowdsec', icon: ShieldAlert, label: 'CrowdSec', daemon: 'crowdsec' as const },
|
||||
{ to: '/dhcp', icon: Wifi, label: 'DHCP', daemon: 'dnsmasq' as const },
|
||||
{ to: '/dns-filter', icon: ShieldBan, label: 'DNS Filter', daemon: 'dnsmasq' as const },
|
||||
];
|
||||
|
||||
const advancedItems = [
|
||||
@@ -101,6 +103,7 @@ export function AppSidebar() {
|
||||
{ to: '/advanced/nftables', icon: Shield, label: 'nftables', daemon: 'nftables' as const },
|
||||
{ to: '/advanced/wireguard', icon: Lock, label: 'WireGuard', daemon: 'wireguard' as const },
|
||||
{ to: '/advanced/crowdsec', icon: ShieldAlert, label: 'CrowdSec', daemon: 'crowdsec' as const },
|
||||
{ to: '/advanced/authelia', icon: KeyRound, label: 'Authelia', daemon: 'authelia' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
633
web/src/components/AutheliaComponent.tsx
Normal file
633
web/src/components/AutheliaComponent.tsx
Normal file
@@ -0,0 +1,633 @@
|
||||
import { useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { KeyRound, BookOpen, CheckCircle, AlertCircle, Plus, Trash2, Loader2, RotateCw, ExternalLink, Copy, X, UserPlus, Globe, Pencil } from 'lucide-react';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from './ui/card';
|
||||
import { Button } from './ui/button';
|
||||
import { Input } from './ui/input';
|
||||
import { Label } from './ui/label';
|
||||
import { Badge } from './ui/badge';
|
||||
import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Switch } from './ui/switch';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||
import { useAuthelia } from '../hooks/useAuthelia';
|
||||
import { domainsApi } from '../services/api';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
export function AutheliaComponent() {
|
||||
const {
|
||||
status, isLoadingStatus,
|
||||
users, isLoadingUsers,
|
||||
oidcClients, isLoadingClients,
|
||||
updateConfig, restart,
|
||||
createUser, deleteUser,
|
||||
createOIDCClient, updateOIDCClient, deleteOIDCClient,
|
||||
} = useAuthelia();
|
||||
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
const [showAddUser, setShowAddUser] = useState(false);
|
||||
const [showAddClient, setShowAddClient] = useState(false);
|
||||
const [newClientSecret, setNewClientSecret] = useState<string | null>(null);
|
||||
const [editingClientId, setEditingClientId] = useState<string | null>(null);
|
||||
|
||||
const showSuccess = (msg: string) => {
|
||||
setSuccessMsg(msg);
|
||||
setErrorMsg(null);
|
||||
setTimeout(() => setSuccessMsg(null), 5000);
|
||||
};
|
||||
|
||||
const showError = (msg: string) => {
|
||||
setErrorMsg(msg);
|
||||
setSuccessMsg(null);
|
||||
};
|
||||
|
||||
if (isLoadingStatus) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
<div className="p-2 bg-primary/10 rounded-lg">
|
||||
<KeyRound className="h-6 w-6 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold">Authentication</h2>
|
||||
<p className="text-muted-foreground">Centralized authentication for network services via Authelia</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Alerts */}
|
||||
{successMsg && (
|
||||
<Alert className="border-green-200 bg-green-50 dark:border-green-800 dark:bg-green-950/20">
|
||||
<CheckCircle className="h-4 w-4 text-green-600" />
|
||||
<AlertDescription className="text-green-700 dark:text-green-300">{successMsg}</AlertDescription>
|
||||
<Button variant="ghost" size="sm" className="absolute right-2 top-2 h-6 w-6 p-0" onClick={() => setSuccessMsg(null)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
{errorMsg && (
|
||||
<Alert variant="error">
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>{errorMsg}</AlertDescription>
|
||||
<Button variant="ghost" size="sm" className="absolute right-2 top-2 h-6 w-6 p-0" onClick={() => setErrorMsg(null)}>
|
||||
<X className="h-3 w-3" />
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{/* Educational Card */}
|
||||
<Card className="bg-gradient-to-r from-cyan-50 to-blue-50 dark:from-cyan-900/20 dark:to-blue-900/20 border-cyan-200 dark:border-cyan-800">
|
||||
<CardContent className="p-4 flex items-start gap-3">
|
||||
<BookOpen className="h-5 w-5 text-cyan-600 dark:text-cyan-400 mt-0.5 shrink-0" />
|
||||
<div className="text-sm text-cyan-800 dark:text-cyan-200">
|
||||
<p className="font-medium mb-1">Centralized Authentication for Your Network</p>
|
||||
<p>Authelia provides two ways to protect services: <strong>Forward-auth</strong> intercepts requests at HAProxy for apps without native login. <strong>OIDC</strong> lets apps like Gitea and Grafana use Authelia as their identity provider. Both share one user database and login portal.</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* Configuration */}
|
||||
<ConfigCard
|
||||
status={status}
|
||||
onUpdate={updateConfig}
|
||||
showSuccess={showSuccess}
|
||||
showError={showError}
|
||||
/>
|
||||
|
||||
{/* Service Status (only if enabled) */}
|
||||
{status?.enabled && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">Service Status</CardTitle>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant={status.active ? 'success' : 'destructive'} className="gap-1">
|
||||
{status.active ? <CheckCircle className="h-3 w-3" /> : <AlertCircle className="h-3 w-3" />}
|
||||
{status.active ? 'Running' : 'Stopped'}
|
||||
</Badge>
|
||||
<Button variant="outline" size="sm" className="gap-1" onClick={() => restart.mutate(undefined, {
|
||||
onSuccess: () => showSuccess('Authelia restarted'),
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to restart'),
|
||||
})} disabled={restart.isPending}>
|
||||
{restart.isPending ? <Loader2 className="h-3 w-3 animate-spin" /> : <RotateCw className="h-3 w-3" />}
|
||||
Restart
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
{status.version && <div><span className="text-muted-foreground">Version</span><div className="font-mono mt-0.5">{status.version}</div></div>}
|
||||
<div><span className="text-muted-foreground">Users</span><div className="font-mono mt-0.5">{status.userCount}</div></div>
|
||||
<div><span className="text-muted-foreground">OIDC Clients</span><div className="font-mono mt-0.5">{status.clientCount}</div></div>
|
||||
{status.domain && (
|
||||
<div>
|
||||
<span className="text-muted-foreground">Login Portal</span>
|
||||
<div className="font-mono mt-0.5">
|
||||
<a href={`https://${status.domain}`} target="_blank" rel="noopener noreferrer" className="text-primary hover:underline inline-flex items-center gap-1">
|
||||
{status.domain}<ExternalLink className="h-3 w-3" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* User Management */}
|
||||
{status?.enabled && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">Users</CardTitle>
|
||||
<Button size="sm" className="gap-1" onClick={() => setShowAddUser(!showAddUser)}>
|
||||
{showAddUser ? <X className="h-3 w-3" /> : <UserPlus className="h-3 w-3" />}
|
||||
{showAddUser ? 'Cancel' : 'Add User'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
{showAddUser && (
|
||||
<AddUserForm
|
||||
onSubmit={(data) => {
|
||||
createUser.mutate(data, {
|
||||
onSuccess: () => { showSuccess(`User "${data.username}" created`); setShowAddUser(false); },
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to create user'),
|
||||
});
|
||||
}}
|
||||
isSubmitting={createUser.isPending}
|
||||
/>
|
||||
)}
|
||||
{isLoadingUsers ? (
|
||||
<div className="text-center py-4"><Loader2 className="h-5 w-5 animate-spin mx-auto text-muted-foreground" /></div>
|
||||
) : users.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No users yet. Add a user to get started.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{users.map((user) => (
|
||||
<div key={user.username} className="flex items-center justify-between p-3 bg-muted/50 rounded-md">
|
||||
<div className="flex items-center gap-3">
|
||||
<div>
|
||||
<span className="font-medium text-sm">{user.displayname || user.username}</span>
|
||||
<span className="text-muted-foreground text-sm ml-2">({user.username})</span>
|
||||
{user.email && <span className="text-muted-foreground text-xs ml-2">{user.email}</span>}
|
||||
</div>
|
||||
{user.disabled && <Badge variant="secondary">Disabled</Badge>}
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive h-7 w-7 p-0"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete user "${user.username}"?`)) {
|
||||
deleteUser.mutate(user.username, {
|
||||
onSuccess: () => showSuccess(`User "${user.username}" deleted`),
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to delete user'),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* OIDC Clients */}
|
||||
{status?.enabled && (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">OIDC Clients</CardTitle>
|
||||
<Button size="sm" className="gap-1" onClick={() => { setShowAddClient(!showAddClient); setNewClientSecret(null); }}>
|
||||
{showAddClient ? <X className="h-3 w-3" /> : <Plus className="h-3 w-3" />}
|
||||
{showAddClient ? 'Cancel' : 'Add Client'}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
{newClientSecret && (
|
||||
<Alert className="border-amber-200 bg-amber-50 dark:border-amber-800 dark:bg-amber-950/20">
|
||||
<AlertDescription className="text-amber-800 dark:text-amber-200">
|
||||
<p className="font-medium mb-1">Client secret — save this now, it won't be shown again:</p>
|
||||
<div className="flex items-center gap-2">
|
||||
<code className="bg-muted px-2 py-1 rounded text-xs font-mono break-all">{newClientSecret}</code>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 shrink-0" onClick={() => navigator.clipboard.writeText(newClientSecret)}>
|
||||
<Copy className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
{showAddClient && (
|
||||
<AddOIDCClientForm
|
||||
onSubmit={(data) => {
|
||||
createOIDCClient.mutate(data, {
|
||||
onSuccess: (result) => {
|
||||
setNewClientSecret(result.clientSecret);
|
||||
setShowAddClient(false);
|
||||
showSuccess(`OIDC client "${data.clientId}" created`);
|
||||
},
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to create client'),
|
||||
});
|
||||
}}
|
||||
isSubmitting={createOIDCClient.isPending}
|
||||
/>
|
||||
)}
|
||||
{isLoadingClients ? (
|
||||
<div className="text-center py-4"><Loader2 className="h-5 w-5 animate-spin mx-auto text-muted-foreground" /></div>
|
||||
) : oidcClients.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4">No OIDC clients. Apps with native SSO support (Gitea, Grafana, etc.) need a client registration here.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{oidcClients.map((client) => (
|
||||
editingClientId === client.clientId ? (
|
||||
<EditOIDCClientForm
|
||||
key={client.clientId}
|
||||
client={client}
|
||||
onSubmit={(data) => {
|
||||
updateOIDCClient.mutate({ clientId: client.clientId, ...data }, {
|
||||
onSuccess: () => { showSuccess(`Client "${client.clientName}" updated`); setEditingClientId(null); },
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to update client'),
|
||||
});
|
||||
}}
|
||||
onCancel={() => setEditingClientId(null)}
|
||||
isSubmitting={updateOIDCClient.isPending}
|
||||
/>
|
||||
) : (
|
||||
<div key={client.clientId} className="flex items-center justify-between p-3 bg-muted/50 rounded-md">
|
||||
<div>
|
||||
<span className="font-medium text-sm">{client.clientName}</span>
|
||||
<span className="text-muted-foreground text-xs ml-2 font-mono">{client.clientId}</span>
|
||||
<div className="text-xs text-muted-foreground mt-0.5 font-mono">
|
||||
{client.redirectUris.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="secondary">{client.authorizationPolicy}</Badge>
|
||||
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => setEditingClientId(client.clientId)}>
|
||||
<Pencil className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" className="text-destructive hover:text-destructive h-7 w-7 p-0"
|
||||
onClick={() => {
|
||||
if (confirm(`Delete OIDC client "${client.clientName}"?`)) {
|
||||
deleteOIDCClient.mutate(client.clientId, {
|
||||
onSuccess: () => showSuccess(`Client "${client.clientName}" deleted`),
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to delete client'),
|
||||
});
|
||||
}
|
||||
}}>
|
||||
<Trash2 className="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Protected Domains */}
|
||||
{status?.enabled && <ProtectedDomainsCard authDomain={status.domain} />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// --- Sub-components ---
|
||||
|
||||
function ConfigCard({ status, onUpdate, showSuccess, showError }: {
|
||||
status: AutheliaStatus | undefined;
|
||||
onUpdate: ReturnType<typeof useAuthelia>['updateConfig'];
|
||||
showSuccess: (msg: string) => void;
|
||||
showError: (msg: string) => void;
|
||||
}) {
|
||||
const [editing, setEditing] = useState(false);
|
||||
const { register, handleSubmit, watch, setValue } = useForm({
|
||||
defaultValues: {
|
||||
domain: status?.domain ?? '',
|
||||
defaultPolicy: status?.defaultPolicy || 'one_factor',
|
||||
},
|
||||
values: status ? {
|
||||
domain: status.domain ?? '',
|
||||
defaultPolicy: status.defaultPolicy || 'one_factor',
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
const enabled = status?.enabled ?? false;
|
||||
const isUpdating = onUpdate.isPending;
|
||||
|
||||
const doUpdate = (data: { enabled?: boolean; domain?: string; defaultPolicy?: string }) => {
|
||||
onUpdate.mutate(data, {
|
||||
onSuccess: () => {
|
||||
setEditing(false);
|
||||
showSuccess('Configuration updated');
|
||||
},
|
||||
onError: (e) => showError(e instanceof Error ? e.message : 'Failed to update config'),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<CardTitle className="text-base">Configuration</CardTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<Label htmlFor="authelia-enabled" className="text-sm">Enabled</Label>
|
||||
<Switch
|
||||
id="authelia-enabled"
|
||||
checked={enabled}
|
||||
onCheckedChange={(checked) => {
|
||||
if (!checked || status?.domain) {
|
||||
doUpdate({ enabled: checked });
|
||||
} else {
|
||||
setEditing(true);
|
||||
}
|
||||
}}
|
||||
disabled={isUpdating || (!status?.installed && !enabled)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0">
|
||||
{!status?.installed && !enabled && (
|
||||
<p className="text-sm text-muted-foreground">Authelia is not installed. Install it first: <code className="bg-muted px-1 rounded">apt install authelia</code></p>
|
||||
)}
|
||||
{(enabled || editing) && (
|
||||
<form onSubmit={handleSubmit((data) => doUpdate({ ...data, enabled: true }))} className="space-y-3">
|
||||
<div>
|
||||
<Label htmlFor="auth-domain">Auth Portal Domain</Label>
|
||||
<Input {...register('domain', { required: 'Required' })} id="auth-domain" placeholder="auth.example.com" className="mt-1 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="default-policy">Default Policy</Label>
|
||||
<Select value={watch('defaultPolicy')} onValueChange={(v) => setValue('defaultPolicy', v)}>
|
||||
<SelectTrigger className="mt-1" id="default-policy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="one_factor">One Factor (password only)</SelectItem>
|
||||
<SelectItem value="two_factor">Two Factor (password + TOTP)</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{editing && (
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" size="sm" disabled={isUpdating}>
|
||||
{isUpdating && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Enable & Save
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={() => setEditing(false)} disabled={isUpdating}>Cancel</Button>
|
||||
</div>
|
||||
)}
|
||||
{enabled && !editing && (
|
||||
<Button type="submit" size="sm" disabled={isUpdating}>
|
||||
{isUpdating && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Save Changes
|
||||
</Button>
|
||||
)}
|
||||
</form>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
function AddUserForm({ onSubmit, isSubmitting }: {
|
||||
onSubmit: (data: { username: string; displayname: string; password: string; email?: string }) => void;
|
||||
isSubmitting: boolean;
|
||||
}) {
|
||||
const { register, handleSubmit, formState: { errors } } = useForm({
|
||||
defaultValues: { username: '', displayname: '', password: '', email: '' },
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="p-3 bg-muted/30 rounded-md space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="new-username">Username</Label>
|
||||
<Input {...register('username', { required: 'Required' })} id="new-username" className="mt-1" />
|
||||
{errors.username && <p className="text-sm text-red-600 mt-1">{errors.username.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="new-displayname">Display Name</Label>
|
||||
<Input {...register('displayname', { required: 'Required' })} id="new-displayname" className="mt-1" />
|
||||
{errors.displayname && <p className="text-sm text-red-600 mt-1">{errors.displayname.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="new-email">Email</Label>
|
||||
<Input {...register('email')} id="new-email" type="email" className="mt-1" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="new-password">Password</Label>
|
||||
<Input {...register('password', { required: 'Required', minLength: { value: 8, message: 'Min 8 characters' } })} id="new-password" type="password" className="mt-1" />
|
||||
{errors.password && <p className="text-sm text-red-600 mt-1">{errors.password.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<Button type="submit" size="sm" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Create User
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function AddOIDCClientForm({ onSubmit, isSubmitting }: {
|
||||
onSubmit: (data: { clientId: string; clientName: string; redirectUris: string[]; authorizationPolicy?: string }) => void;
|
||||
isSubmitting: boolean;
|
||||
}) {
|
||||
const { register, handleSubmit, formState: { errors }, watch, setValue } = useForm({
|
||||
defaultValues: { clientId: '', clientName: '', redirectUri: '', authorizationPolicy: 'one_factor' },
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((data) => {
|
||||
onSubmit({
|
||||
clientId: data.clientId,
|
||||
clientName: data.clientName,
|
||||
redirectUris: [data.redirectUri],
|
||||
authorizationPolicy: data.authorizationPolicy,
|
||||
});
|
||||
})} className="p-3 bg-muted/30 rounded-md space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label htmlFor="client-id">Client ID</Label>
|
||||
<Input {...register('clientId', { required: 'Required' })} id="client-id" placeholder="gitea" className="mt-1 font-mono" />
|
||||
{errors.clientId && <p className="text-sm text-red-600 mt-1">{errors.clientId.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="client-name">Client Name</Label>
|
||||
<Input {...register('clientName', { required: 'Required' })} id="client-name" placeholder="Gitea" className="mt-1" />
|
||||
{errors.clientName && <p className="text-sm text-red-600 mt-1">{errors.clientName.message}</p>}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="redirect-uri">Redirect URI</Label>
|
||||
<Input {...register('redirectUri', { required: 'Required' })} id="redirect-uri" placeholder="https://git.example.com/user/oauth2/authelia/callback" className="mt-1 font-mono text-sm" />
|
||||
{errors.redirectUri && <p className="text-sm text-red-600 mt-1">{errors.redirectUri.message}</p>}
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="client-policy">Authorization Policy</Label>
|
||||
<Select value={watch('authorizationPolicy')} onValueChange={(v) => setValue('authorizationPolicy', v)}>
|
||||
<SelectTrigger className="mt-1" id="client-policy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="one_factor">One Factor</SelectItem>
|
||||
<SelectItem value="two_factor">Two Factor</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<Button type="submit" size="sm" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Create Client
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
function EditOIDCClientForm({ client, onSubmit, onCancel, isSubmitting }: {
|
||||
client: { clientId: string; clientName: string; redirectUris: string[]; authorizationPolicy: string };
|
||||
onSubmit: (data: { clientName?: string; redirectUris?: string[]; authorizationPolicy?: string }) => void;
|
||||
onCancel: () => void;
|
||||
isSubmitting: boolean;
|
||||
}) {
|
||||
const { register, handleSubmit, watch, setValue } = useForm({
|
||||
defaultValues: {
|
||||
clientName: client.clientName,
|
||||
redirectUri: client.redirectUris[0] ?? '',
|
||||
authorizationPolicy: client.authorizationPolicy || 'one_factor',
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit((data) => {
|
||||
onSubmit({
|
||||
clientName: data.clientName,
|
||||
redirectUris: [data.redirectUri],
|
||||
authorizationPolicy: data.authorizationPolicy,
|
||||
});
|
||||
})} className="p-3 bg-muted/30 rounded-md space-y-3">
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div>
|
||||
<Label>Client ID</Label>
|
||||
<Input value={client.clientId} disabled className="mt-1 font-mono" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-client-name">Client Name</Label>
|
||||
<Input {...register('clientName')} id="edit-client-name" className="mt-1" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-redirect-uri">Redirect URI</Label>
|
||||
<Input {...register('redirectUri')} id="edit-redirect-uri" className="mt-1 font-mono text-sm" />
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="edit-client-policy">Authorization Policy</Label>
|
||||
<Select value={watch('authorizationPolicy')} onValueChange={(v) => setValue('authorizationPolicy', v)}>
|
||||
<SelectTrigger className="mt-1" id="edit-client-policy">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="one_factor">One Factor</SelectItem>
|
||||
<SelectItem value="two_factor">Two Factor</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button type="submit" size="sm" disabled={isSubmitting}>
|
||||
{isSubmitting && <Loader2 className="h-3 w-3 animate-spin mr-1" />}
|
||||
Save
|
||||
</Button>
|
||||
<Button type="button" variant="outline" size="sm" onClick={onCancel} disabled={isSubmitting}>Cancel</Button>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
interface AutheliaStatus {
|
||||
active: boolean;
|
||||
version?: string;
|
||||
enabled: boolean;
|
||||
domain: string;
|
||||
defaultPolicy: string;
|
||||
userCount: number;
|
||||
clientCount: number;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
function ProtectedDomainsCard({ authDomain }: { authDomain: string }) {
|
||||
const queryClient = useQueryClient();
|
||||
const domainsQuery = useQuery({
|
||||
queryKey: ['domains'],
|
||||
queryFn: () => domainsApi.list(),
|
||||
});
|
||||
|
||||
const toggleAuthMutation = useMutation({
|
||||
mutationFn: async ({ domain, enabled }: { domain: string; enabled: boolean }) => {
|
||||
return apiClient.patch(`/api/v1/domains/${domain}`, { auth: { enabled, policy: 'one_factor' } });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['domains'] });
|
||||
},
|
||||
});
|
||||
|
||||
const httpDomains = (domainsQuery.data?.domains ?? []).filter(
|
||||
(d) => d.backend.type === 'http' && d.source !== 'authelia' && d.source !== 'central' && d.domain !== authDomain
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader className="pb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<CardTitle className="text-base">Protected Domains</CardTitle>
|
||||
<Badge variant="secondary" className="text-xs">Forward-Auth</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-0 space-y-3">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Toggle forward-auth protection for L7 HTTP services. Apps with native OIDC support (Gitea, Grafana, etc.) should use an OIDC client above instead.
|
||||
</p>
|
||||
{httpDomains.length === 0 ? (
|
||||
<p className="text-sm text-muted-foreground text-center py-4 flex items-center justify-center gap-2">
|
||||
<Globe className="h-4 w-4" />
|
||||
No L7 HTTP domains registered yet.
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{httpDomains.map((domain) => {
|
||||
const authEnabled = (domain as any).auth?.enabled ?? false;
|
||||
return (
|
||||
<div key={domain.domain} className="flex items-center justify-between p-3 bg-muted/50 rounded-md">
|
||||
<div>
|
||||
<span className="font-mono text-sm">{domain.domain}</span>
|
||||
<span className="text-muted-foreground text-xs ml-2">({domain.source})</span>
|
||||
</div>
|
||||
<Switch
|
||||
checked={authEnabled}
|
||||
onCheckedChange={(checked) => toggleAuthMutation.mutate({ domain: domain.domain, enabled: checked })}
|
||||
disabled={toggleAuthMutation.isPending}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// Import apiClient for the PATCH call in ProtectedDomainsCard
|
||||
import { apiClient } from '../services/api/client';
|
||||
@@ -7,7 +7,7 @@ import { Alert, AlertDescription } from './ui/alert';
|
||||
import { Badge } from './ui/badge';
|
||||
import {
|
||||
LayoutDashboard, Cloud, CheckCircle, XCircle, AlertCircle, Loader2,
|
||||
BookOpen, Globe, Router, RotateCw, Key,
|
||||
BookOpen, Globe, Router, RotateCw, Key, KeyRound,
|
||||
Shield, Eye, ArrowLeftRight, Lock,
|
||||
} from 'lucide-react';
|
||||
import { useCloudflare } from '../hooks/useCloudflare';
|
||||
@@ -22,6 +22,7 @@ const SERVICES = [
|
||||
{ key: 'nftables', label: 'Firewall', icon: Shield },
|
||||
{ key: 'wireguard', label: 'VPN', icon: Lock },
|
||||
{ key: 'crowdsec', label: 'CrowdSec', icon: Eye },
|
||||
{ key: 'authelia', label: 'Auth', icon: KeyRound },
|
||||
];
|
||||
|
||||
function formatUptime(totalSeconds: number): string {
|
||||
|
||||
41
web/src/components/advanced/AutheliaSubsystem.tsx
Normal file
41
web/src/components/advanced/AutheliaSubsystem.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
import { useState } from 'react';
|
||||
import { useQuery, useMutation } from '@tanstack/react-query';
|
||||
import { KeyRound } from 'lucide-react';
|
||||
import { autheliaApi } from '../../services/api/authelia';
|
||||
import { SubsystemPage } from '../SubsystemPage';
|
||||
|
||||
export function AutheliaSubsystem() {
|
||||
const [successMsg, setSuccessMsg] = useState<string | null>(null);
|
||||
const [errorMsg, setErrorMsg] = useState<string | null>(null);
|
||||
|
||||
const configQuery = useQuery({
|
||||
queryKey: ['authelia', 'config', 'raw'],
|
||||
queryFn: () => autheliaApi.getConfig(),
|
||||
});
|
||||
|
||||
const restartMutation = useMutation({
|
||||
mutationFn: () => autheliaApi.restart(),
|
||||
onSuccess: () => {
|
||||
setSuccessMsg('Authelia restarted');
|
||||
setErrorMsg(null);
|
||||
setTimeout(() => setSuccessMsg(null), 5000);
|
||||
},
|
||||
onError: (e: Error) => setErrorMsg(e.message),
|
||||
});
|
||||
|
||||
return (
|
||||
<SubsystemPage
|
||||
title="Authelia"
|
||||
description="Authentication and identity provider"
|
||||
icon={KeyRound}
|
||||
daemonKey="authelia"
|
||||
configContent={configQuery.data?.content}
|
||||
isLoadingConfig={configQuery.isLoading}
|
||||
onRestart={() => restartMutation.mutate()}
|
||||
isRestarting={restartMutation.isPending}
|
||||
restartLabel="Restart Authelia"
|
||||
successMessage={successMsg}
|
||||
errorMessage={errorMsg}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export { DnsmasqSubsystem } from './DnsmasqSubsystem';
|
||||
export { NftablesSubsystem } from './NftablesSubsystem';
|
||||
export { WireguardSubsystem } from './WireguardSubsystem';
|
||||
export { CrowdsecSubsystem } from './CrowdsecSubsystem';
|
||||
export { AutheliaSubsystem } from './AutheliaSubsystem';
|
||||
|
||||
90
web/src/hooks/useAuthelia.ts
Normal file
90
web/src/hooks/useAuthelia.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { autheliaApi } from '../services/api/authelia';
|
||||
import type { CreateUserRequest, UpdateUserRequest, CreateOIDCClientRequest, UpdateOIDCClientRequest } from '../services/api/authelia';
|
||||
|
||||
export function useAuthelia() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const statusQuery = useQuery({
|
||||
queryKey: ['authelia', 'status'],
|
||||
queryFn: () => autheliaApi.getStatus(),
|
||||
staleTime: 10000,
|
||||
});
|
||||
|
||||
const usersQuery = useQuery({
|
||||
queryKey: ['authelia', 'users'],
|
||||
queryFn: () => autheliaApi.listUsers(),
|
||||
enabled: !!statusQuery.data?.enabled,
|
||||
});
|
||||
|
||||
const oidcClientsQuery = useQuery({
|
||||
queryKey: ['authelia', 'oidc', 'clients'],
|
||||
queryFn: () => autheliaApi.listOIDCClients(),
|
||||
enabled: !!statusQuery.data?.active,
|
||||
});
|
||||
|
||||
const invalidateAll = () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['authelia'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['central', 'status'] });
|
||||
};
|
||||
|
||||
const updateConfigMutation = useMutation({
|
||||
mutationFn: (data: { enabled?: boolean; domain?: string; defaultPolicy?: string }) =>
|
||||
autheliaApi.updateConfig(data),
|
||||
onSuccess: invalidateAll,
|
||||
});
|
||||
|
||||
const restartMutation = useMutation({
|
||||
mutationFn: () => autheliaApi.restart(),
|
||||
onSuccess: invalidateAll,
|
||||
});
|
||||
|
||||
const createUserMutation = useMutation({
|
||||
mutationFn: (req: CreateUserRequest) => autheliaApi.createUser(req),
|
||||
onSuccess: invalidateAll,
|
||||
});
|
||||
|
||||
const updateUserMutation = useMutation({
|
||||
mutationFn: ({ username, ...req }: UpdateUserRequest & { username: string }) =>
|
||||
autheliaApi.updateUser(username, req),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['authelia', 'users'] }),
|
||||
});
|
||||
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: (username: string) => autheliaApi.deleteUser(username),
|
||||
onSuccess: invalidateAll,
|
||||
});
|
||||
|
||||
const createOIDCClientMutation = useMutation({
|
||||
mutationFn: (req: CreateOIDCClientRequest) => autheliaApi.createOIDCClient(req),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['authelia', 'oidc', 'clients'] }),
|
||||
});
|
||||
|
||||
const updateOIDCClientMutation = useMutation({
|
||||
mutationFn: ({ clientId, ...req }: UpdateOIDCClientRequest & { clientId: string }) =>
|
||||
autheliaApi.updateOIDCClient(clientId, req),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['authelia', 'oidc', 'clients'] }),
|
||||
});
|
||||
|
||||
const deleteOIDCClientMutation = useMutation({
|
||||
mutationFn: (clientId: string) => autheliaApi.deleteOIDCClient(clientId),
|
||||
onSuccess: () => queryClient.invalidateQueries({ queryKey: ['authelia', 'oidc', 'clients'] }),
|
||||
});
|
||||
|
||||
return {
|
||||
status: statusQuery.data,
|
||||
isLoadingStatus: statusQuery.isLoading,
|
||||
users: usersQuery.data?.users ?? [],
|
||||
isLoadingUsers: usersQuery.isLoading,
|
||||
oidcClients: oidcClientsQuery.data?.clients ?? [],
|
||||
isLoadingClients: oidcClientsQuery.isLoading,
|
||||
updateConfig: updateConfigMutation,
|
||||
restart: restartMutation,
|
||||
createUser: createUserMutation,
|
||||
updateUser: updateUserMutation,
|
||||
deleteUser: deleteUserMutation,
|
||||
createOIDCClient: createOIDCClientMutation,
|
||||
updateOIDCClient: updateOIDCClientMutation,
|
||||
deleteOIDCClient: deleteOIDCClientMutation,
|
||||
};
|
||||
}
|
||||
10
web/src/router/pages/AutheliaPage.tsx
Normal file
10
web/src/router/pages/AutheliaPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../components';
|
||||
import { AutheliaComponent } from '../../components/AutheliaComponent';
|
||||
|
||||
export function AutheliaPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<AutheliaComponent />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
10
web/src/router/pages/advanced/AutheliaPage.tsx
Normal file
10
web/src/router/pages/advanced/AutheliaPage.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { ErrorBoundary } from '../../../components';
|
||||
import { AutheliaSubsystem } from '../../../components/advanced';
|
||||
|
||||
export function AutheliaAdvancedPage() {
|
||||
return (
|
||||
<ErrorBoundary>
|
||||
<AutheliaSubsystem />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
}
|
||||
@@ -3,3 +3,4 @@ export { DnsmasqPage } from './DnsmasqPage';
|
||||
export { NftablesPage } from './NftablesPage';
|
||||
export { WireguardPage } from './WireguardPage';
|
||||
export { CrowdsecPage } from './CrowdsecPage';
|
||||
export { AutheliaAdvancedPage } from './AutheliaPage';
|
||||
|
||||
@@ -7,9 +7,12 @@ import { FirewallPage } from './pages/FirewallPage';
|
||||
import { VpnPage } from './pages/VpnPage';
|
||||
import { CrowdSecPage } from './pages/CrowdSecPage';
|
||||
import { DhcpPage } from './pages/DhcpPage';
|
||||
import { DnsFilterPage } from './pages/DnsFilterPage';
|
||||
import { AutheliaPage } from './pages/AutheliaPage';
|
||||
import {
|
||||
HaproxyPage, DnsmasqPage, NftablesPage, WireguardPage,
|
||||
CrowdsecPage as CrowdsecAdvancedPage,
|
||||
AutheliaAdvancedPage,
|
||||
} from './pages/advanced';
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
@@ -22,12 +25,15 @@ export const routes: RouteObject[] = [
|
||||
{ path: 'firewall', element: <FirewallPage /> },
|
||||
{ path: 'vpn', element: <VpnPage /> },
|
||||
{ path: 'crowdsec', element: <CrowdSecPage /> },
|
||||
{ path: 'auth', element: <AutheliaPage /> },
|
||||
{ path: 'dhcp', element: <DhcpPage /> },
|
||||
{ path: 'dns-filter', element: <DnsFilterPage /> },
|
||||
{ path: 'advanced/haproxy', element: <HaproxyPage /> },
|
||||
{ path: 'advanced/dnsmasq', element: <DnsmasqPage /> },
|
||||
{ path: 'advanced/nftables', element: <NftablesPage /> },
|
||||
{ path: 'advanced/wireguard', element: <WireguardPage /> },
|
||||
{ path: 'advanced/crowdsec', element: <CrowdsecAdvancedPage /> },
|
||||
{ path: 'advanced/authelia', element: <AutheliaAdvancedPage /> },
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
111
web/src/services/api/authelia.ts
Normal file
111
web/src/services/api/authelia.ts
Normal file
@@ -0,0 +1,111 @@
|
||||
import { apiClient } from './client';
|
||||
|
||||
// Types
|
||||
|
||||
export interface AutheliaStatus {
|
||||
active: boolean;
|
||||
version?: string;
|
||||
enabled: boolean;
|
||||
domain: string;
|
||||
defaultPolicy: string;
|
||||
userCount: number;
|
||||
clientCount: number;
|
||||
installed: boolean;
|
||||
}
|
||||
|
||||
export interface AutheliaUser {
|
||||
username: string;
|
||||
displayname: string;
|
||||
email?: string;
|
||||
groups?: string[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface CreateUserRequest {
|
||||
username: string;
|
||||
displayname: string;
|
||||
password: string;
|
||||
email?: string;
|
||||
groups?: string[];
|
||||
}
|
||||
|
||||
export interface UpdateUserRequest {
|
||||
displayname?: string;
|
||||
password?: string;
|
||||
email?: string;
|
||||
groups?: string[];
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface OIDCClient {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
clientSecret?: string;
|
||||
redirectUris: string[];
|
||||
scopes: string[];
|
||||
authorizationPolicy: string;
|
||||
consentMode?: string;
|
||||
}
|
||||
|
||||
export interface CreateOIDCClientRequest {
|
||||
clientId: string;
|
||||
clientName: string;
|
||||
redirectUris: string[];
|
||||
scopes?: string[];
|
||||
authorizationPolicy?: string;
|
||||
consentMode?: string;
|
||||
}
|
||||
|
||||
export interface UpdateOIDCClientRequest {
|
||||
clientName?: string;
|
||||
redirectUris?: string[];
|
||||
scopes?: string[];
|
||||
authorizationPolicy?: string;
|
||||
consentMode?: string;
|
||||
}
|
||||
|
||||
// API
|
||||
|
||||
export const autheliaApi = {
|
||||
// Status & Config
|
||||
async getStatus(): Promise<AutheliaStatus> {
|
||||
return apiClient.get('/api/v1/authelia/status');
|
||||
},
|
||||
async getConfig(): Promise<{ content: string }> {
|
||||
return apiClient.get('/api/v1/authelia/config');
|
||||
},
|
||||
async updateConfig(data: { enabled?: boolean; domain?: string; defaultPolicy?: string }): Promise<{ message: string }> {
|
||||
return apiClient.put('/api/v1/authelia/config', data);
|
||||
},
|
||||
async restart(): Promise<{ message: string }> {
|
||||
return apiClient.post('/api/v1/authelia/restart');
|
||||
},
|
||||
|
||||
// Users
|
||||
async listUsers(): Promise<{ users: AutheliaUser[] }> {
|
||||
return apiClient.get('/api/v1/authelia/users');
|
||||
},
|
||||
async createUser(req: CreateUserRequest): Promise<{ message: string }> {
|
||||
return apiClient.post('/api/v1/authelia/users', req);
|
||||
},
|
||||
async updateUser(username: string, req: UpdateUserRequest): Promise<{ message: string }> {
|
||||
return apiClient.put(`/api/v1/authelia/users/${username}`, req);
|
||||
},
|
||||
async deleteUser(username: string): Promise<{ message: string }> {
|
||||
return apiClient.delete(`/api/v1/authelia/users/${username}`);
|
||||
},
|
||||
|
||||
// OIDC Clients
|
||||
async listOIDCClients(): Promise<{ clients: OIDCClient[] }> {
|
||||
return apiClient.get('/api/v1/authelia/oidc/clients');
|
||||
},
|
||||
async createOIDCClient(req: CreateOIDCClientRequest): Promise<{ clientId: string; clientSecret: string; message: string }> {
|
||||
return apiClient.post('/api/v1/authelia/oidc/clients', req);
|
||||
},
|
||||
async updateOIDCClient(clientId: string, req: UpdateOIDCClientRequest): Promise<{ message: string }> {
|
||||
return apiClient.put(`/api/v1/authelia/oidc/clients/${clientId}`, req);
|
||||
},
|
||||
async deleteOIDCClient(clientId: string): Promise<{ message: string }> {
|
||||
return apiClient.delete(`/api/v1/authelia/oidc/clients/${clientId}`);
|
||||
},
|
||||
};
|
||||
@@ -11,3 +11,7 @@ export { cloudflareApi } from './cloudflare';
|
||||
export type { CloudflareVerifyResponse, CloudflareZone } from './cloudflare';
|
||||
export { dnsSettingsApi, ddnsConfigApi, dhcpConfigApi, nftablesConfigApi, haproxyRoutesApi } from './settings';
|
||||
export type { OperatorSettings, CentralDomainSettings, DNSSettings, DDNSConfig, DHCPConfig, NftablesConfig, HAProxyCustomRoute, HAProxyRoutes } from './settings';
|
||||
export { autheliaApi } from './authelia';
|
||||
export type { AutheliaStatus, AutheliaUser, OIDCClient } from './authelia';
|
||||
export { dnsFilterApi } from './dnsfilter';
|
||||
export type { DNSFilterStats, DNSFilterConfig, Blocklist, CustomEntry } from './dnsfilter';
|
||||
|
||||
@@ -30,6 +30,10 @@ export interface RegisteredDomain {
|
||||
public: boolean;
|
||||
tls?: string;
|
||||
routes?: Route[];
|
||||
auth?: {
|
||||
enabled: boolean;
|
||||
policy?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export const domainsApi = {
|
||||
|
||||
Reference in New Issue
Block a user