Business services (backend/app/services/)¶
Services contain reusable logic, independent of the HTTP layer. Routers orchestrate them but never implement business logic themselves.
wireguard.py¶
All interactions with the WireGuard system tools (wg, wg-quick) and network tools (ip).
| Function | Role |
|---|---|
_run(*args, stdin=None) |
Runs a command without a shell (create_subprocess_exec), so that no value derived from user input (key, name, IP) can be interpreted by a shell. Raises WireGuardError on a non-zero return code. |
generate_keypair() |
wg genkey + wg pubkey → private/public key pair. |
get_service_state() |
running/stopped, inferred from wg showconf. |
start_service() / stop_service() / restart_service() |
Control the interface via wg-quick up/down. |
add_peer() / remove_peer() |
Adds/removes a peer without restarting the interface (wg set ... peer ...), with adjustment of the associated route. |
reload_peers() |
Hot-reloads the peer list (wg-quick strip + wg syncconf), without dropping existing connections — used after every client create/update/delete. |
get_status() |
Parses the wg show output into a structure usable by the API (state, peers, transfer, last handshake). |
build_client_config() |
Generates the text content of a client's .conf file ([Interface]/[Peer]), from the client, server, and global settings. |
build_server_config() |
Generates the server-side wg0.conf content, including all active peers (enabled=True). |
write_server_config() |
Writes build_server_config() to disk (/etc/wireguard/wg0.conf), in an executor so as not to block the asyncio loop. |
get_machine_ips() |
Lists the non-loopback IPs of the host machine (ip -j address show, falling back to socket.getaddrinfo). |
seed.py¶
seed_initial_data() — called on every application startup (see lifespan), idempotent:
_seed_roles(): creates theadmin(admin-read,admin-write,user-read,user-write) anduser(user-read,user-write) roles if they don't exist._seed_admin(): only if no user exists yet, creates the admin account (ADMIN_USERNAME/ADMIN_EMAIL) with a random password (secrets.token_urlsafe(16)), shown once in the logs (WARNINGlevel)._seed_settings()/_seed_oidc_settings()/_seed_smtp_settings(): create the default settings rows if absent.
audit.py¶
log_event(db, action, *, actor=None, target=None, details=None, request=None, success=True): inserts an event (AuditLog) and commits immediately, independently of the caller's transaction — so the event is recorded even if the action itself subsequently fails.actorcan be aUserobject (itsusernameis extracted) or a raw string (useful for logging a failed login attempt with an invalid username)._prune(db): called after every insertion. Deletes events older thanAUDIT_RETENTION_DAYS, then, if the total count still exceedsAUDIT_MAX_EVENTS, deletes the oldest excess events.
Actions logged in the code: auth.login, auth.login_failed, auth.logout, auth.password_changed, client.created/updated/deleted, server.updated/reset/apply/service.<action>, global_settings.updated/reset, smtp_settings.updated, oidc_settings.updated/reset, user.created/updated/deleted, pat.created/revoked.
pat.py¶
Generation and validation of Personal Access Tokens.
generate_raw_token():wgui_pat_+ 32 random bytes astoken_urlsafe. Returns(raw_token, prefix)— the prefix (about the first 17 characters) serves as a human-readable identifier without exposing the secret.hash_token(raw_token): SHA-256 of the token — only the hash is stored in the database (no salt: tokens already have high entropy).expires_at_for(duration): converts a code (7d,30d,90d,1y,unlimited) into an expiration date.resolve_user_from_pat(db, raw_token): finds the active user owning a valid token (not expired, not revoked), updateslast_used_at. Used byauth.py(get_current_user) when a token starts with thewgui_pat_prefix.
users.py¶
load_roles(db, role_ids): resolves a list of role IDs intoRoleobjects; raises400if the list is empty.count_active_admins(db): counts active users with theadminrole.ensure_not_last_active_admin(db, user): raises400ifuseris the last active administrator — called before any deletion/deactivation/demotion.
auth.py (service, not to be confused with app/auth.py)¶
local_login_allowed(db): returnsFalseif OIDC-only mode is enabled (OidcSettings.enabled and oidc_only), thereby blocking local password login.authenticate_user(db, username, password): verifies credentials; explicitly rejectsauth_source == "oidc"accounts (they have no usable local password) with the same generic message as invalid credentials, so as not to disclose an account's authentication method.
oidc.py¶
All OIDC / OpenID Connect logic. Detailed in Authentication & security.
smtp.py / settings.py¶
CRUD for the singleton SmtpSettings and GlobalSettings settings:
get_or_create_smtp_settings(db)/get_or_create_settings(db): retrieves the existing row or creates it with default values.build_smtp_response(): builds the response schema, always excluding the password.build_smtp_update_dict(): builds the update dictionary, keeping the existing password if the payload doesn't provide a new one (avoids overwriting an already-saved secret with an empty value).SMTP_DEFAULTS/SETTINGS_DEFAULTS: values used by theDELETE /resetendpoints.
email.py¶
send_client_config_email(): sends the configuration email to a client — renders the Jinja2 template matching the language (client_config_en/fr/es.html, inbackend/app/templates/), generates the inline QR code, attaches the.conffile._resolve_mail_from(): determines the valid sender address (from_address, falling back to the SMTPusername), raises a clear error if neither is a valid email address.- Uses
fastapi-mailfor actual SMTP sending (configurable TLS/SSL).
ip_suggestion.py¶
suggest_next_ip(server_cidr, allocated_ips): computes the next free IP in the server's network — reserves the server's own address (first usable host of the CIDR), excludes IPs already allocated to clients, returns the first still-free address (or None if the network is full or the CIDR is invalid).
qr.py¶
generate_qr_code_base64(content): generates a PNG QR code from the text content of a .conf file, base64-encoded — used both by the API (GET /api/clients/{id}/config) and by the configuration email.
Dependency overview¶
flowchart TD
Routers["routers/*.py"] --> Seed[seed.py]
Routers --> Audit[audit.py]
Routers --> PAT[pat.py]
Routers --> Users[users.py]
Routers --> AuthSvc["auth.py (service)"]
Routers --> Oidc[oidc.py]
Routers --> Smtp[smtp.py]
Routers --> Settings[settings.py]
Routers --> Email[email.py]
Routers --> IpSugg[ip_suggestion.py]
Routers --> Qr[qr.py]
Routers --> Wg[wireguard.py]
Email --> Qr
Email --> Smtp
AuthSvc --> Oidc
Wg -->|subprocess| System["wg / wg-quick / ip"]