fix: allow using self-signed certs

Closes https://github.com/etiennecollin/unifi-voucher-manager/issues/3
This commit is contained in:
etiennecollin
2025-08-15 09:21:43 -04:00
parent 40edea1d74
commit 0f0a0cc51a
7 changed files with 70 additions and 27 deletions

View File

@@ -115,22 +115,23 @@ Make sure to configure the required variables. The optional variables generally
To configure the WiFi QR code, you are required to configure the `WIFI_SSID` and `WIFI_PASSWORD` variables.
| Variable | Type | Description | Example | Type |
| ------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------------------- |
| `UNIFI_CONTROLLER_URL` | Required | URL to your UniFi controller with protocol. | `https://unifi.example.com:8443` | `string` |
| `UNIFI_API_KEY` | Required | API Key for your UniFi controller. | `abc123...` | `string` |
| `UNIFI_SITE_ID` | Optional | Site ID of your UniFi controller. Using the value `default`, the backend will try to fetch the ID of the default site. | `default` (default) | `string` |
| `FRONTEND_BIND_HOST` | Optional | Address on which the frontend server binds. | `0.0.0.0` (default) | `IPv4` |
| `FRONTEND_BIND_PORT` | Optional | Port on which the frontend server binds. | `3000` (default) | `u16` |
| `FRONTEND_TO_BACKEND_URL` | Optional | URL where the frontend will make its API requests to the backend. | `http://127.0.0.1` (default) | `URL` |
| `BACKEND_BIND_HOST` | Optional | Address on which the server binds. | `127.0.0.1` (default) | `IPv4` |
| `BACKEND_BIND_PORT` | Optional | Port on which the backend server binds. | `8080` (default) | `u16` |
| `BACKEND_LOG_LEVEL` | Optional | Log level of the Rust backend. | `info`(default) | `trace\|debug\|info\|warn\|error` |
| `TIMEZONE` | Optional | [Timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) used to format dates and time. | `UTC` (default) | [`timezone`](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) |
| `WIFI_SSID` | Optional | WiFi SSID used for the QR code. (required for QR code to be generated) | `My WiFi SSID` | `string` |
| `WIFI_PASSWORD` | Optional | WiFi password used for the QR code (required for QR code to be generated) | `My WiFi Password` | `string` |
| `WIFI_TYPE` | Optional | WiFi security type used. Defaults to `WPA` if a password is provided and `nopass` otherwise. | `WPA` | `WPA\|WEP\|nopass` |
| `WIFI_HIDDEN` | Optional | Whether the WiFi SSID is hidden or broadcasted. | `false` (default) | `bool` |
| Variable | Type | Description | Example | Type |
| ------------------------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------- |
| `UNIFI_CONTROLLER_URL` | Required | URL to your UniFi controller with protocol (`http://` or `https://`). | `https://unifi.example.com` or `https://192.168.8.1:443` | `string` |
| `UNIFI_API_KEY` | Required | API Key for your UniFi controller. | `abc123...` | `string` |
| `UNIFI_HAS_VALID_CERT` | Optional | Whether your UniFi controller has a valid, non-self signed certificate. **If you directly use the controller IP address in the URL, this should probably be set to `false`.** | `true` (default) | `bool` |
| `UNIFI_SITE_ID` | Optional | Site ID of your UniFi controller. Using the value `default`, the backend will try to fetch the ID of the default site. | `default` (default) | `string` |
| `FRONTEND_BIND_HOST` | Optional | Address on which the frontend server binds. | `0.0.0.0` (default) | `IPv4` |
| `FRONTEND_BIND_PORT` | Optional | Port on which the frontend server binds. | `3000` (default) | `u16` |
| `FRONTEND_TO_BACKEND_URL` | Optional | URL where the frontend will make its API requests to the backend. | `http://127.0.0.1` (default) | `URL` |
| `BACKEND_BIND_HOST` | Optional | Address on which the server binds. | `127.0.0.1` (default) | `IPv4` |
| `BACKEND_BIND_PORT` | Optional | Port on which the backend server binds. | `8080` (default) | `u16` |
| `BACKEND_LOG_LEVEL` | Optional | Log level of the Rust backend. | `info`(default) | `trace\|debug\|info\|warn\|error` |
| `TIMEZONE` | Optional | [Timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) used to format dates and time. | `UTC` (default) | [`timezone`](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List) |
| `WIFI_SSID` | Optional | WiFi SSID used for the QR code. (required for QR code to be generated) | `My WiFi SSID` | `string` |
| `WIFI_PASSWORD` | Optional | WiFi password used for the QR code (required for QR code to be generated) | `My WiFi Password` | `string` |
| `WIFI_TYPE` | Optional | WiFi security type used. Defaults to `WPA` if a password is provided and `nopass` otherwise. | `WPA` | `WPA\|WEP\|nopass` |
| `WIFI_HIDDEN` | Optional | Whether the WiFi SSID is hidden or broadcasted. | `false` (default) | `bool` |
### Getting UniFi API Credentials

View File

@@ -20,6 +20,7 @@ pub struct Environment {
pub unifi_api_key: String,
pub backend_bind_host: String,
pub backend_bind_port: u16,
pub unifi_has_valid_cert: bool,
pub timezone: Tz,
}
@@ -30,6 +31,13 @@ impl Environment {
let unifi_controller_url: String =
env::var("UNIFI_CONTROLLER_URL").map_err(|e| format!("UNIFI_CONTROLLER_URL: {e}"))?;
if !unifi_controller_url.starts_with("http://")
&& !unifi_controller_url.starts_with("https://")
{
return Err("UNIFI_CONTROLLER_URL must start with http:// or https://".to_string());
}
let unifi_api_key: String =
env::var("UNIFI_API_KEY").map_err(|e| format!("UNIFI_API_KEY: {e}"))?;
let unifi_site_id: String =
@@ -44,6 +52,17 @@ impl Environment {
Err(_) => DEFAULT_BACKEND_BIND_PORT,
};
let unifi_has_valid_cert: bool = match env::var("UNIFI_HAS_VALID_CERT") {
Ok(val) => match val.trim().to_lowercase().as_str() {
"true" | "1" | "yes" => true,
"false" | "0" | "no" => false,
_ => {
return Err("Invalid UNIFI_HAS_VALID_CERT, must be true/false".to_string());
}
},
Err(_) => true,
};
let timezone: Tz = match env::var("TIMEZONE") {
Ok(s) => match s.parse() {
Ok(tz) => {
@@ -67,6 +86,7 @@ impl Environment {
unifi_api_key,
backend_bind_host,
backend_bind_port,
unifi_has_valid_cert,
timezone,
})
}

View File

@@ -2,7 +2,7 @@ use chrono::DateTime;
use chrono_tz::Tz;
use reqwest::{Client, ClientBuilder, StatusCode};
use std::{sync::OnceLock, time::Duration};
use tracing::{error, info, warn};
use tracing::{debug, error, info, warn};
use crate::{
ENVIRONMENT, Environment,
@@ -52,6 +52,7 @@ impl<'a> UnifiAPI<'a> {
.timeout(Duration::from_secs(30))
.connect_timeout(Duration::from_secs(10))
.default_headers(headers)
.danger_accept_invalid_certs(!environment.unifi_has_valid_cert)
.use_rustls_tls()
.build()
.expect("Failed to build UniFi reqwest client");
@@ -113,7 +114,11 @@ impl<'a> UnifiAPI<'a> {
Ok(resp) => resp,
Err(e) => {
let status = e.status().unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
error!("Request failed with status {}: {}", status, e.without_url());
error!(
"Request failed with status {}: {:?}",
status,
e.without_url()
);
return Err(status);
}
};
@@ -134,9 +139,24 @@ impl<'a> UnifiAPI<'a> {
}
};
// It's a successful response, now parse the JSON
clean_response.json::<U>().await.map_err(|e| {
error!("Failed to parse response JSON: {}", e);
// It's a successful response, now get the response body
let response_text = clean_response.text().await.map_err(|e| {
error!("Failed to read response body: {:?}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
// Parse the response body as JSON
let response_json: serde_json::Value =
serde_json::from_str(&response_text).map_err(|e| {
error!("Failed to parse response body as JSON: {:?}", e);
debug!("Response body: {}", response_text);
StatusCode::INTERNAL_SERVER_ERROR
})?;
// Parse the JSON into the expected structure
serde_json::from_value::<U>(response_json).map_err(|e| {
error!("Failed to parse response JSON structure: {:?}", e);
debug!("Response body: {}", response_text);
StatusCode::INTERNAL_SERVER_ERROR
})
}

View File

@@ -11,10 +11,12 @@ services:
restart: "unless-stopped"
ports:
- "3000:3000"
# SEE README FOR VALID ENVIRONMENT VARIABLES
environment:
UNIFI_CONTROLLER_URL: "URL to your UniFi controller with protocol (`http://` or `https://`)."
UNIFI_API_KEY: "API Key for your UniFi controller."
UNIFI_HAS_VALID_CERT: "true" # Set to false if your UniFi controller has a self-signed certificate
# To put the environment variables in a `.env` file, uncomment the env_file
# section and comment the environment section.
# env_file:
# - ".env"
environment:
UNIFI_CONTROLLER_URL: "URL to your Unifi controller with protocol. Example: 'https://unifi.example.com:8443' or 'http://192.168.0.1'"
UNIFI_API_KEY: "API Key for your Unifi controller"

View File

@@ -15,7 +15,7 @@ export default function NotificationItem({ id, message, type, onDone }: Props) {
useEffect(() => {
// slide in next tick
const showTimer = setTimeout(() => setVisible(true), 10);
// slide out after 4s
// slide out after X ms
const hideTimer = setTimeout(
() => setVisible(false),
NOTIFICATION_DURATION_MS,

View File

@@ -38,7 +38,7 @@ export default function CustomCreateTab() {
setNewCode(code);
form.reset();
} else {
notify("Voucher created, but code not available", "error");
notify("Voucher created, but code not found in response", "warning");
}
} catch {
notify("Failed to create voucher", "error");

View File

@@ -31,7 +31,7 @@ export default function QuickCreateTab() {
setNewCode(code);
form.reset();
} else {
notify("Voucher created, but code not available", "error");
notify("Voucher created, but code not found in response", "warning");
}
} catch {
notify("Failed to create voucher", "error");