updateInterfaceLdapFilters saved the matched users with SaveInterface,
which creates the interface when it is missing.
On first start that panics. The sync runs immediately (main.go:85) and
the importer later (main.go:116), so the sync creates a stub row for
every interface_filter key, and the importer, which snapshotted the
interface list before its device round-trips, then fails with "interface
already exists". The window is GetInterfaces plus GetPeers, so a
directory on localhost loses the race and a slower one hides it.
The stub rows are wrong anyway. They have no backend, so a typo in an
interface_filter key quietly created an interface attached to no
controller.
Look the interface up and skip with a warning when it is absent. The
filter is applied on the next sync once the importer has created it. A
lookup error that is not ErrNotFound also skips.
Signed-off-by: clark-ja <37738506+clark-ja@users.noreply.github.com>
disable_missing tested absence against the raw sync result without
checking that the search returned anything usable, so a search that
succeeds and yields nothing looked like "every user has been removed".
Connection and search errors were already safe, since synchronizeLdapUsers
returns before the disable phase. The gap is the successful-but-empty
case: a base_dn or sync_filter that stops matching, an unpopulated
replica, a field_map user_identifier naming an attribute the server does
not return, or a bind account that lost read access to the user subtree.
LDAP gives nothing to tell those apart from a directory that is genuinely
empty; they all answer success with zero entries.
Acting on it is not a database flag. TopicUserDisabled removes each
user's peers from the WireGuard device, the successful search means no
error is logged, and every message on the path was Debug while log_level
defaults to info, so the whole event was silent. It also repeats every
sync interval.
Refuse to disable anyone when no usable identifier came back, logging the
provider, entry count and identifier field. The guard counts identifiers,
not entries, so it covers the field_map case too. The per-user disable
line moves from Debug to Warn so a mass disable is audible even where the
guard does not fire.
The cost is that a directory intentionally emptied of users now disables
nobody. That is documented, along with the workaround: leave one account
matching sync_filter and everyone else is disabled as before.
Signed-off-by: clark-ja <37738506+clark-ja@users.noreply.github.com>
* Add dynamic Mikrotik peer support & UI/i18n
Introduce handling for dynamically created Mikrotik peers: add Dynamic/IsDynamic fields to domain, peer and interface models and API models; include "dynamic" in Mikrotik queries; skip updates/deletes for dynamic peers; sync dynamic flag when restoring state. Prevent modifying/deleting dynamic peers in manager. UI: disable selection/edit for dynamic peers and show badge. Implement Mikrotik interface hook execution. Add i18n keys/translations for dynamic-peer messages across languages.
* Fix Mikrotik hooks implementation and UI visibility
* fix: build OAuth return URL from the live location, not the Vite base
On base_path deployments, OAuth/OIDC login failed with a 400 "invalid
return URL" before the user ever reached the IdP.
LoginView and the router derived the app's runtime URL mount from
import.meta.env.BASE_URL, which is Vite's build-time *asset* base. Since
#711 set `base: './'` to fix relative asset loading (#710), BASE_URL is
"./", so `base_path: /wg` produced the return URL https://host/wg./#/login.
isValidReturnUrl() requires <base_path>/app, so every external-auth user
on a base_path deployment was locked out.
Derive the return URL from window.location instead: the app is mounted at
{base_path}/app/ in production and at / under `npm run dev`, so the live
document location is the only reliable source. Likewise drop the explicit
base from createWebHashHistory(), which then defaults to
`location.pathname + location.search` -- correct in every deployment.
This leaves the relative asset base from #711 untouched, so #710 stays
fixed, and it removes the last two readers of import.meta.env.BASE_URL
under frontend/src so the asset base can no longer affect routing.
Fixes#719
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dan Berg <dan@webinargeek.com>
* fix: log rejected OAuth return URLs
The isValidReturnUrl() rejection was the only failure branch in
handleOauthInitiateGet without a slog call, and the 400 response body
reveals neither what was received nor what was expected. That is what
made #719 hard to diagnose.
Log both at Debug level, matching the neighbouring branches. The response
body is unchanged -- the URL is not echoed to the client.
Also note on the base-path test that the URL shape it pins is produced by
externalLogin() in frontend/src/views/LoginView.vue, so a future frontend
change has a breadcrumb back to the contract.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Dan Berg <dan@webinargeek.com>
---------
Signed-off-by: Dan Berg <dan@webinargeek.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Remove hardcoded `--base=/app/` from npm build script and set `base:
'./'` in vite.config to generate relative asset URLs
Update server `updateBasePathInFrontend` to handle relative paths
Fixes#710
Signed-off-by: Rich C <richcarni@gmail.com>
The OIDC client was only extracting claims from the ID token, but many
OIDC providers (like Authelia) don't include all user information in the
ID token. Fields like 'preferred_username' are typically only available
via the userinfo endpoint.
This fix fetches additional user information from the provider's userinfo
endpoint and merges it with the ID token claims, ensuring that all
required user fields are available for user registration and login.
Fixes#697
Signed-off-by: Aram Akhavan <1147328+kaysond@users.noreply.github.com>
* Add pfSense backend domain types and configuration
This adds the necessary domain types and configuration structures
for the pfSense backend support. Includes PfsenseInterfaceExtras and
PfsensePeerExtras structs, and the BackendPfsense configuration
with API URL, key, and timeout settings.
* Add low-level pfSense REST API client
Implements the HTTP client for interacting with the pfSense REST API.
Handles authentication via X-API-Key header, request/response parsing,
and error handling. Uses the pfSense REST API v2 endpoints as documented
at https://pfrest.org/.
* Implement pfSense WireGuard controller
This implements the InterfaceController interface for pfSense firewalls.
Handles WireGuard tunnel and peer management through the pfSense REST API.
Includes proper filtering of peers by interface (since API filtering doesn't
work) and parsing of the allowedips array structure with address/mask fields.
* Register pfSense controllers and update configuration
Registers the pfSense backend controllers in the controller manager
and adds example configuration to config.yml.sample. Also updates
README to mention pfSense backend support.
* Fix peer filtering and allowedips parsing for pfSense backend
The pfSense REST API doesn't support filtering peers by interface
via query parameters, so all peers are returned regardless of the
filter. This caused peers from all interfaces to be randomly assigned
to a single interface in wg-portal.
Additionally, the API returns allowedips as an array of objects with
"address" and "mask" fields instead of a comma-separated string,
which caused parsing failures.
Changes:
- Remove API filter from GetPeers() since it doesn't work
- Add client-side filtering by checking the "tun" field in peer responses
- Update convertWireGuardPeer() to parse allowedips array structure
- Add parseAddressArray() helper for parsing address objects
- Attempt to fetch interface addresses from /tunnel/{id}/address endpoint
(endpoint may not be available in all pfSense versions)
- Add debug logging for peer filtering and address loading operations
Note: Interface addresses may still be empty if the address endpoint
is not available. Public Endpoint and Default DNS Servers are typically
configured manually in wg-portal as the pfSense API doesn't provide
this information.
* Extract endpoint, DNS, and peer names from pfSense peer data
The pfSense API provides endpoint, port, and description (descr) fields
in peer responses that can be used to populate interface defaults and
peer display names.
Changes:
- Extract endpoint and port from peers and combine them properly
- Fix peer name/description extraction to check "descr" field first
(pfSense API uses "descr" instead of "description" or "comment")
- Add extractPfsenseDefaultsFromPeers() helper to extract common
endpoint and DNS from peers during interface import
- Set PeerDefEndpoint and PeerDefDnsStr from peer data for pfSense
backends during interface import
- Use most common endpoint/DNS values when multiple peers are present
* Fix interface display name to use descr field from pfSense API
The pfSense API uses "descr" field for tunnel descriptions, not
"description" or "comment". Updated convertWireGuardInterface()
to check "descr" first so that tunnel descriptions (e.g., "HQ VPN")
are displayed in the UI instead of just the tunnel name (e.g., "tun_wg0").
* Remove calls to non-working tunnel and peer detail endpoints
The pfSense REST API endpoints /api/v2/vpn/wireguard/tunnel/{id}
and /api/v2/vpn/wireguard/tunnel/{id}/address don't work and were
causing log spam. Removed these calls and use only the data from
the tunnel/peer list responses.
Also removed the peer detail endpoint call that was added for
statistics collection, as it likely doesn't work either.
* Fix unused variable compilation error
Removed unused deviceId variable that was causing build failure.
* Optimize tunnel address fetching to use /tunnel?id endpoint
Instead of using the separate /tunnel/address endpoint, now query
the specific tunnel endpoint /tunnel?id={id} which includes the
addresses array in the response. This avoids unnecessary API calls
and simplifies the code.
- GetInterface() now queries /tunnel?id={id} after getting tunnel ID
- loadInterfaceData() queries /tunnel?id={id} as fallback if addresses missing
- extractAddresses() properly parses addresses array from tunnel response
- Removed /tunnel/address endpoint calls
Signed-off-by: rwjack <jack@foss.family>
* Fix URL encoding issue in tunnel endpoint queries
Use Filters in PfsenseRequestOptions instead of passing query strings
directly in the path. This prevents the ? character from being encoded
as %3F, which was causing 404 errors.
- GetInterface() now uses Filters map for id parameter
- loadInterfaceData() now uses Filters map for id parameter
Signed-off-by: rwjack <jack@foss.family>
* update backend docs for pfsense
---------
Signed-off-by: rwjack <jack@foss.family>
* mikrotik: allow to set DNS, wip: handle routes in wg-controller
* replace old route handling for local controller
* cleanup route handling for local backend
* implement route handling for mikrotik controller