Add SMTP support for Authelia notifications and fix config persistence
SMTP: Add host, port, username, sender, and password fields to the Authelia configuration. Uses modern address format (submission:// for STARTTLS on 587, smtps:// for implicit TLS on 465) with explicit TLS server name. Falls back to filesystem notifier when SMTP is not configured. Fixes: Config changes now persist before attempting service restart, so a restart failure (e.g. bad SMTP credentials) no longer prevents saving. The specific Authelia error is extracted from the journal and shown in the UI. Frontend: SMTP fields added to the Authentication config card. Form no longer continuously resets from server state while user is editing. OIDC client edit UI added (pencil icon).
This commit is contained in:
@@ -31,6 +31,19 @@ func (api *API) AutheliaStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
defaultPolicy = state.Cloud.Authelia.DefaultPolicy
|
defaultPolicy = state.Cloud.Authelia.DefaultPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
|
smtp := struct {
|
||||||
|
Host string `json:"host"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Sender string `json:"sender"`
|
||||||
|
}{}
|
||||||
|
if state != nil {
|
||||||
|
smtp.Host = state.Cloud.Authelia.SMTP.Host
|
||||||
|
smtp.Port = state.Cloud.Authelia.SMTP.Port
|
||||||
|
smtp.Username = state.Cloud.Authelia.SMTP.Username
|
||||||
|
smtp.Sender = state.Cloud.Authelia.SMTP.Sender
|
||||||
|
}
|
||||||
|
|
||||||
respondJSON(w, http.StatusOK, map[string]any{
|
respondJSON(w, http.StatusOK, map[string]any{
|
||||||
"active": status.Active,
|
"active": status.Active,
|
||||||
"version": status.Version,
|
"version": status.Version,
|
||||||
@@ -40,6 +53,7 @@ func (api *API) AutheliaStatus(w http.ResponseWriter, r *http.Request) {
|
|||||||
"userCount": api.authelia.UserCount(),
|
"userCount": api.authelia.UserCount(),
|
||||||
"clientCount": api.authelia.OIDCClientCount(),
|
"clientCount": api.authelia.OIDCClientCount(),
|
||||||
"installed": api.authelia.IsInstalled(),
|
"installed": api.authelia.IsInstalled(),
|
||||||
|
"smtp": smtp,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +73,11 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
Enabled *bool `json:"enabled"`
|
Enabled *bool `json:"enabled"`
|
||||||
Domain *string `json:"domain"`
|
Domain *string `json:"domain"`
|
||||||
DefaultPolicy *string `json:"defaultPolicy"`
|
DefaultPolicy *string `json:"defaultPolicy"`
|
||||||
|
SMTPHost *string `json:"smtpHost"`
|
||||||
|
SMTPPort *int `json:"smtpPort"`
|
||||||
|
SMTPUsername *string `json:"smtpUsername"`
|
||||||
|
SMTPSender *string `json:"smtpSender"`
|
||||||
|
SMTPPassword *string `json:"smtpPassword"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||||
respondError(w, http.StatusBadRequest, "Invalid JSON")
|
respondError(w, http.StatusBadRequest, "Invalid JSON")
|
||||||
@@ -79,6 +98,21 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
if req.Enabled != nil {
|
if req.Enabled != nil {
|
||||||
state.Cloud.Authelia.Enabled = *req.Enabled
|
state.Cloud.Authelia.Enabled = *req.Enabled
|
||||||
}
|
}
|
||||||
|
if req.SMTPHost != nil {
|
||||||
|
state.Cloud.Authelia.SMTP.Host = *req.SMTPHost
|
||||||
|
}
|
||||||
|
if req.SMTPPort != nil {
|
||||||
|
state.Cloud.Authelia.SMTP.Port = *req.SMTPPort
|
||||||
|
}
|
||||||
|
if req.SMTPUsername != nil {
|
||||||
|
state.Cloud.Authelia.SMTP.Username = *req.SMTPUsername
|
||||||
|
}
|
||||||
|
if req.SMTPSender != nil {
|
||||||
|
state.Cloud.Authelia.SMTP.Sender = *req.SMTPSender
|
||||||
|
}
|
||||||
|
if req.SMTPPassword != nil && *req.SMTPPassword != "" {
|
||||||
|
_ = api.secrets.SetSecret("authelia.smtpPassword", *req.SMTPPassword)
|
||||||
|
}
|
||||||
|
|
||||||
// Enabling Authelia
|
// Enabling Authelia
|
||||||
if state.Cloud.Authelia.Enabled {
|
if state.Cloud.Authelia.Enabled {
|
||||||
@@ -132,27 +166,36 @@ func (api *API) AutheliaUpdateConfig(w http.ResponseWriter, r *http.Request) {
|
|||||||
api.ensureAuthDomain(state.Cloud.Authelia.Domain)
|
api.ensureAuthDomain(state.Cloud.Authelia.Domain)
|
||||||
|
|
||||||
// Start service — requires at least one user
|
// Start service — requires at least one user
|
||||||
|
startErr := error(nil)
|
||||||
if api.authelia.UserCount() > 0 {
|
if api.authelia.UserCount() > 0 {
|
||||||
if err := api.authelia.RestartService(); err != nil {
|
startErr = api.authelia.RestartService()
|
||||||
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Failed to start Authelia: %v", err))
|
}
|
||||||
return
|
|
||||||
}
|
// Save state and reconcile regardless of whether the service started
|
||||||
|
if err := config.SaveState(state, api.statePath()); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "Failed to save state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go api.reconcileNetworking()
|
||||||
|
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
|
||||||
|
|
||||||
|
if startErr != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, fmt.Sprintf("Configuration saved but Authelia failed to start: %v", startErr))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Disabling — stop service and deregister domain
|
// Disabling — stop service and deregister domain
|
||||||
_ = api.authelia.StopService()
|
_ = api.authelia.StopService()
|
||||||
api.deregisterAuthDomain()
|
api.deregisterAuthDomain()
|
||||||
|
|
||||||
|
if err := config.SaveState(state, api.statePath()); err != nil {
|
||||||
|
respondError(w, http.StatusInternalServerError, "Failed to save state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go api.reconcileNetworking()
|
||||||
|
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := config.SaveState(state, api.statePath()); err != nil {
|
|
||||||
respondError(w, http.StatusInternalServerError, "Failed to save state")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trigger reconciliation to update HAProxy
|
|
||||||
go api.reconcileNetworking()
|
|
||||||
|
|
||||||
api.broadcastAutheliaEvent("authelia:config", "Authelia configuration updated")
|
|
||||||
respondMessage(w, http.StatusOK, "Authelia configuration updated")
|
respondMessage(w, http.StatusOK, "Authelia configuration updated")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -382,6 +425,8 @@ func (api *API) generateAutheliaConfig(state *config.State) error {
|
|||||||
|
|
||||||
sessionDomain := authelia.SessionDomainFromAuthDomain(state.Cloud.Authelia.Domain)
|
sessionDomain := authelia.SessionDomainFromAuthDomain(state.Cloud.Authelia.Domain)
|
||||||
|
|
||||||
|
smtpPassword, _ := api.secrets.GetSecret("authelia.smtpPassword")
|
||||||
|
|
||||||
opts := authelia.ConfigOpts{
|
opts := authelia.ConfigOpts{
|
||||||
Domain: state.Cloud.Authelia.Domain,
|
Domain: state.Cloud.Authelia.Domain,
|
||||||
JWTSecret: jwtSecret,
|
JWTSecret: jwtSecret,
|
||||||
@@ -391,6 +436,11 @@ func (api *API) generateAutheliaConfig(state *config.State) error {
|
|||||||
DefaultPolicy: defaultPolicy,
|
DefaultPolicy: defaultPolicy,
|
||||||
SessionDomain: sessionDomain,
|
SessionDomain: sessionDomain,
|
||||||
OIDCClients: clients,
|
OIDCClients: clients,
|
||||||
|
SMTPHost: state.Cloud.Authelia.SMTP.Host,
|
||||||
|
SMTPPort: state.Cloud.Authelia.SMTP.Port,
|
||||||
|
SMTPUsername: state.Cloud.Authelia.SMTP.Username,
|
||||||
|
SMTPSender: state.Cloud.Authelia.SMTP.Sender,
|
||||||
|
SMTPPassword: smtpPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
return api.authelia.GenerateConfig(opts)
|
return api.authelia.GenerateConfig(opts)
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ type ConfigOpts struct {
|
|||||||
DataDir string // authelia data directory path
|
DataDir string // authelia data directory path
|
||||||
ListenAddr string // listen address (default: 127.0.0.1:9091)
|
ListenAddr string // listen address (default: 127.0.0.1:9091)
|
||||||
OIDCClients []OIDCClient // registered OIDC clients
|
OIDCClients []OIDCClient // registered OIDC clients
|
||||||
|
SMTPHost string // SMTP server host (empty = use filesystem notifier)
|
||||||
|
SMTPPort int // SMTP server port (default: 587)
|
||||||
|
SMTPUsername string // SMTP login username (defaults to sender if empty)
|
||||||
|
SMTPSender string // From address
|
||||||
|
SMTPPassword string // SMTP password (from secrets)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GenerateConfig builds Authelia's configuration.yml and writes it atomically.
|
// GenerateConfig builds Authelia's configuration.yml and writes it atomically.
|
||||||
@@ -143,11 +148,7 @@ func (m *Manager) buildConfig(opts ConfigOpts) map[string]any {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
"notifier": map[string]any{
|
"notifier": buildNotifier(opts, m.notificationPath()),
|
||||||
"filesystem": map[string]any{
|
|
||||||
"filename": m.notificationPath(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
"identity_validation": map[string]any{
|
"identity_validation": map[string]any{
|
||||||
"reset_password": map[string]any{
|
"reset_password": map[string]any{
|
||||||
@@ -202,6 +203,40 @@ func (m *Manager) buildConfig(opts ConfigOpts) map[string]any {
|
|||||||
return cfg
|
return cfg
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// buildNotifier returns the notifier config — SMTP if configured, filesystem otherwise.
|
||||||
|
func buildNotifier(opts ConfigOpts, filesystemPath string) map[string]any {
|
||||||
|
if opts.SMTPHost != "" && opts.SMTPSender != "" {
|
||||||
|
username := opts.SMTPUsername
|
||||||
|
if username == "" {
|
||||||
|
username = opts.SMTPSender
|
||||||
|
}
|
||||||
|
port := opts.SMTPPort
|
||||||
|
if port == 0 {
|
||||||
|
port = 587
|
||||||
|
}
|
||||||
|
// Use the modern address format: submission:// for STARTTLS on 587, smtps:// for implicit TLS on 465
|
||||||
|
scheme := "submission"
|
||||||
|
if port == 465 {
|
||||||
|
scheme = "smtps"
|
||||||
|
}
|
||||||
|
smtp := map[string]any{
|
||||||
|
"address": fmt.Sprintf("%s://%s:%d", scheme, opts.SMTPHost, port),
|
||||||
|
"sender": opts.SMTPSender,
|
||||||
|
"username": username,
|
||||||
|
"password": opts.SMTPPassword,
|
||||||
|
"tls": map[string]any{
|
||||||
|
"server_name": opts.SMTPHost,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return map[string]any{"smtp": smtp}
|
||||||
|
}
|
||||||
|
return map[string]any{
|
||||||
|
"filesystem": map[string]any{
|
||||||
|
"filename": filesystemPath,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// GenerateSecret creates a cryptographically random hex string of the given byte length.
|
// GenerateSecret creates a cryptographically random hex string of the given byte length.
|
||||||
func GenerateSecret(byteLen int) (string, error) {
|
func GenerateSecret(byteLen int) (string, error) {
|
||||||
b := make([]byte, byteLen)
|
b := make([]byte, byteLen)
|
||||||
|
|||||||
@@ -95,17 +95,66 @@ func (m *Manager) GetStatus() (*Status, error) {
|
|||||||
return status, nil
|
return status, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RestartService restarts the Authelia systemd service
|
// RestartService restarts the Authelia systemd service and verifies it stays running.
|
||||||
func (m *Manager) RestartService() error {
|
func (m *Manager) RestartService() error {
|
||||||
cmd := exec.Command("systemctl", "restart", "authelia.service")
|
cmd := exec.Command("systemctl", "restart", "authelia.service")
|
||||||
output, err := cmd.CombinedOutput()
|
output, err := cmd.CombinedOutput()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to restart authelia: %w (output: %s)", err, string(output))
|
return fmt.Errorf("failed to restart authelia: %w (output: %s)", err, string(output))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Give it a moment to start (or crash)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
|
||||||
|
// Check if it's actually running
|
||||||
|
if exec.Command("systemctl", "is-active", "--quiet", "authelia.service").Run() != nil {
|
||||||
|
// It crashed — grab the error from the journal
|
||||||
|
reason := m.lastError()
|
||||||
|
if reason != "" {
|
||||||
|
return fmt.Errorf("authelia started but crashed: %s", reason)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("authelia started but crashed (check journalctl -u authelia.service)")
|
||||||
|
}
|
||||||
|
|
||||||
slog.Info("authelia service restarted", "component", "authelia")
|
slog.Info("authelia service restarted", "component", "authelia")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// lastError extracts the most recent error-level message from the Authelia journal.
|
||||||
|
// Authelia logs look like: time="..." level=error msg="..." error="..." provider=...
|
||||||
|
func (m *Manager) lastError() string {
|
||||||
|
cmd := exec.Command("journalctl", "-u", "authelia.service", "-n", "20", "--no-pager", "-o", "cat")
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Find the last line containing level=error
|
||||||
|
var lastErr string
|
||||||
|
for _, line := range strings.Split(string(output), "\n") {
|
||||||
|
if strings.Contains(line, "level=error") {
|
||||||
|
lastErr = line
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if lastErr == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
// Extract error="..." field (has the actual detail)
|
||||||
|
if i := strings.Index(lastErr, "error=\""); i >= 0 {
|
||||||
|
detail := lastErr[i+7:]
|
||||||
|
if j := strings.Index(detail, "\""); j >= 0 {
|
||||||
|
return detail[:j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall back to msg="..." field
|
||||||
|
if i := strings.Index(lastErr, "msg=\""); i >= 0 {
|
||||||
|
detail := lastErr[i+5:]
|
||||||
|
if j := strings.Index(detail, "\""); j >= 0 {
|
||||||
|
return detail[:j]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
// StopService stops the Authelia systemd service
|
// StopService stops the Authelia systemd service
|
||||||
func (m *Manager) StopService() error {
|
func (m *Manager) StopService() error {
|
||||||
cmd := exec.Command("systemctl", "stop", "authelia.service")
|
cmd := exec.Command("systemctl", "stop", "authelia.service")
|
||||||
|
|||||||
@@ -118,6 +118,12 @@ type State struct {
|
|||||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||||
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // login portal domain (e.g. auth.payne.io)
|
Domain string `yaml:"domain,omitempty" json:"domain,omitempty"` // login portal domain (e.g. auth.payne.io)
|
||||||
DefaultPolicy string `yaml:"defaultPolicy,omitempty" json:"defaultPolicy,omitempty"` // one_factor or two_factor
|
DefaultPolicy string `yaml:"defaultPolicy,omitempty" json:"defaultPolicy,omitempty"` // one_factor or two_factor
|
||||||
|
SMTP struct {
|
||||||
|
Host string `yaml:"host,omitempty" json:"host,omitempty"` // e.g. smtp.gmail.com
|
||||||
|
Port int `yaml:"port,omitempty" json:"port,omitempty"` // e.g. 587
|
||||||
|
Username string `yaml:"username,omitempty" json:"username,omitempty"` // SMTP login username
|
||||||
|
Sender string `yaml:"sender,omitempty" json:"sender,omitempty"` // From address (e.g. auth@payne.io)
|
||||||
|
} `yaml:"smtp,omitempty" json:"smtp,omitempty"`
|
||||||
} `yaml:"authelia,omitempty" json:"authelia,omitempty"`
|
} `yaml:"authelia,omitempty" json:"authelia,omitempty"`
|
||||||
DNSFilter struct {
|
DNSFilter struct {
|
||||||
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
Enabled bool `yaml:"enabled,omitempty" json:"enabled,omitempty"`
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { Switch } from './ui/switch';
|
|||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from './ui/select';
|
||||||
import { useAuthelia } from '../hooks/useAuthelia';
|
import { useAuthelia } from '../hooks/useAuthelia';
|
||||||
import { domainsApi } from '../services/api';
|
import { domainsApi } from '../services/api';
|
||||||
|
import type { AutheliaStatus } from '../services/api/authelia';
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
export function AutheliaComponent() {
|
export function AutheliaComponent() {
|
||||||
@@ -312,21 +313,37 @@ function ConfigCard({ status, onUpdate, showSuccess, showError }: {
|
|||||||
showError: (msg: string) => void;
|
showError: (msg: string) => void;
|
||||||
}) {
|
}) {
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const { register, handleSubmit, watch, setValue } = useForm({
|
const [initialized, setInitialized] = useState(false);
|
||||||
|
const { register, handleSubmit, watch, setValue, reset } = useForm({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
domain: status?.domain ?? '',
|
domain: '',
|
||||||
defaultPolicy: status?.defaultPolicy || 'one_factor',
|
defaultPolicy: 'one_factor',
|
||||||
|
smtpHost: '',
|
||||||
|
smtpPort: 587,
|
||||||
|
smtpUsername: '',
|
||||||
|
smtpSender: '',
|
||||||
|
smtpPassword: '',
|
||||||
},
|
},
|
||||||
values: status ? {
|
});
|
||||||
|
|
||||||
|
// Sync form with status data once on load, not continuously
|
||||||
|
if (status && !initialized) {
|
||||||
|
reset({
|
||||||
domain: status.domain ?? '',
|
domain: status.domain ?? '',
|
||||||
defaultPolicy: status.defaultPolicy || 'one_factor',
|
defaultPolicy: status.defaultPolicy || 'one_factor',
|
||||||
} : undefined,
|
smtpHost: status.smtp?.host ?? '',
|
||||||
});
|
smtpPort: status.smtp?.port || 587,
|
||||||
|
smtpUsername: status.smtp?.username ?? '',
|
||||||
|
smtpSender: status.smtp?.sender ?? '',
|
||||||
|
smtpPassword: '',
|
||||||
|
});
|
||||||
|
setInitialized(true);
|
||||||
|
}
|
||||||
|
|
||||||
const enabled = status?.enabled ?? false;
|
const enabled = status?.enabled ?? false;
|
||||||
const isUpdating = onUpdate.isPending;
|
const isUpdating = onUpdate.isPending;
|
||||||
|
|
||||||
const doUpdate = (data: { enabled?: boolean; domain?: string; defaultPolicy?: string }) => {
|
const doUpdate = (data: { enabled?: boolean; domain?: string; defaultPolicy?: string; smtpHost?: string; smtpPort?: number; smtpUsername?: string; smtpSender?: string; smtpPassword?: string }) => {
|
||||||
onUpdate.mutate(data, {
|
onUpdate.mutate(data, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setEditing(false);
|
setEditing(false);
|
||||||
@@ -363,7 +380,11 @@ function ConfigCard({ status, onUpdate, showSuccess, showError }: {
|
|||||||
<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>
|
<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) && (
|
{(enabled || editing) && (
|
||||||
<form onSubmit={handleSubmit((data) => doUpdate({ ...data, enabled: true }))} className="space-y-3">
|
<form onSubmit={handleSubmit((data) => {
|
||||||
|
const update: any = { ...data, enabled: true };
|
||||||
|
if (!update.smtpPassword) delete update.smtpPassword; // don't send empty password
|
||||||
|
doUpdate(update);
|
||||||
|
})} className="space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="auth-domain">Auth Portal Domain</Label>
|
<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" />
|
<Input {...register('domain', { required: 'Required' })} id="auth-domain" placeholder="auth.example.com" className="mt-1 font-mono" />
|
||||||
@@ -380,6 +401,34 @@ function ConfigCard({ status, onUpdate, showSuccess, showError }: {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="pt-2 border-t">
|
||||||
|
<p className="text-sm font-medium mb-2">Email Notifications (SMTP)</p>
|
||||||
|
<p className="text-xs text-muted-foreground mb-3">Required for password resets and 2FA enrollment. Without SMTP, notifications are written to a local file.</p>
|
||||||
|
<div className="grid grid-cols-3 gap-3">
|
||||||
|
<div className="col-span-2">
|
||||||
|
<Label htmlFor="smtp-host">SMTP Host</Label>
|
||||||
|
<Input {...register('smtpHost')} id="smtp-host" placeholder="smtp.gmail.com" className="mt-1 font-mono" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="smtp-port">Port</Label>
|
||||||
|
<Input {...register('smtpPort', { valueAsNumber: true })} id="smtp-port" type="number" placeholder="587" className="mt-1 font-mono" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-3 gap-3 mt-3">
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="smtp-sender">Sender Email</Label>
|
||||||
|
<Input {...register('smtpSender')} id="smtp-sender" placeholder="auth@example.com" className="mt-1 font-mono" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="smtp-username">Username</Label>
|
||||||
|
<Input {...register('smtpUsername')} id="smtp-username" placeholder="(defaults to sender)" className="mt-1 font-mono" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label htmlFor="smtp-password">Password</Label>
|
||||||
|
<Input {...register('smtpPassword')} id="smtp-password" type="password" placeholder={status?.smtp?.host ? '••••••••' : ''} className="mt-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{editing && (
|
{editing && (
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Button type="submit" size="sm" disabled={isUpdating}>
|
<Button type="submit" size="sm" disabled={isUpdating}>
|
||||||
@@ -556,17 +605,6 @@ function EditOIDCClientForm({ client, onSubmit, onCancel, isSubmitting }: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AutheliaStatus {
|
|
||||||
active: boolean;
|
|
||||||
version?: string;
|
|
||||||
enabled: boolean;
|
|
||||||
domain: string;
|
|
||||||
defaultPolicy: string;
|
|
||||||
userCount: number;
|
|
||||||
clientCount: number;
|
|
||||||
installed: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ProtectedDomainsCard({ authDomain }: { authDomain: string }) {
|
function ProtectedDomainsCard({ authDomain }: { authDomain: string }) {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const domainsQuery = useQuery({
|
const domainsQuery = useQuery({
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ export function useAuthelia() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateConfigMutation = useMutation({
|
const updateConfigMutation = useMutation({
|
||||||
mutationFn: (data: { enabled?: boolean; domain?: string; defaultPolicy?: string }) =>
|
mutationFn: (data: { enabled?: boolean; domain?: string; defaultPolicy?: string; smtpHost?: string; smtpPort?: number; smtpUsername?: string; smtpSender?: string; smtpPassword?: string }) =>
|
||||||
autheliaApi.updateConfig(data),
|
autheliaApi.updateConfig(data),
|
||||||
onSuccess: invalidateAll,
|
onSuccess: invalidateAll,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,12 @@ export interface AutheliaStatus {
|
|||||||
userCount: number;
|
userCount: number;
|
||||||
clientCount: number;
|
clientCount: number;
|
||||||
installed: boolean;
|
installed: boolean;
|
||||||
|
smtp: {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
username: string;
|
||||||
|
sender: string;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AutheliaUser {
|
export interface AutheliaUser {
|
||||||
@@ -74,7 +80,7 @@ export const autheliaApi = {
|
|||||||
async getConfig(): Promise<{ content: string }> {
|
async getConfig(): Promise<{ content: string }> {
|
||||||
return apiClient.get('/api/v1/authelia/config');
|
return apiClient.get('/api/v1/authelia/config');
|
||||||
},
|
},
|
||||||
async updateConfig(data: { enabled?: boolean; domain?: string; defaultPolicy?: string }): Promise<{ message: string }> {
|
async updateConfig(data: { enabled?: boolean; domain?: string; defaultPolicy?: string; smtpHost?: string; smtpPort?: number; smtpUsername?: string; smtpSender?: string; smtpPassword?: string }): Promise<{ message: string }> {
|
||||||
return apiClient.put('/api/v1/authelia/config', data);
|
return apiClient.put('/api/v1/authelia/config', data);
|
||||||
},
|
},
|
||||||
async restart(): Promise<{ message: string }> {
|
async restart(): Promise<{ message: string }> {
|
||||||
|
|||||||
Reference in New Issue
Block a user