diff --git a/admin-web/src/api/client.ts b/admin-web/src/api/client.ts index 606fe6e..4d92db8 100644 --- a/admin-web/src/api/client.ts +++ b/admin-web/src/api/client.ts @@ -5,6 +5,7 @@ export type User = { id: string; username: string; display_name: string; + email?: string; role: string; is_active: boolean; }; @@ -109,6 +110,21 @@ export const api = { method: "POST", body: JSON.stringify(payload) }), + updateUser: (userId: string, payload: { + display_name?: string; + email?: string; + role?: string; + password?: string; + is_active?: boolean; + }) => + request(`/admin/users/${userId}`, { + method: "PATCH", + body: JSON.stringify(payload) + }), + deleteUser: (userId: string) => + request<{ ok: boolean }>(`/admin/users/${userId}`, { + method: "DELETE" + }), devices: () => request("/admin/devices"), deviceProfile: (deviceId: string) => request(`/admin/devices/${deviceId}/profile`), revokeDevice: (deviceId: string) => @@ -135,6 +151,24 @@ export const api = { method: "POST", body: JSON.stringify(payload) }), + updatePolicy: (policyId: string, payload: { + name?: string; + description?: string; + priority?: number; + effect?: string; + full_tunnel?: boolean; + is_active?: boolean; + destinations?: string[]; + targets?: Array<{ type: string; id: string }>; + }) => + request(`/admin/policies/${policyId}`, { + method: "PATCH", + body: JSON.stringify(payload) + }), + deletePolicy: (policyId: string) => + request<{ ok: boolean }>(`/admin/policies/${policyId}`, { + method: "DELETE" + }), gateways: () => request("/admin/gateways"), updateGateway: (gatewayId: string, payload: { endpoint: string; diff --git a/admin-web/src/features/policies/PoliciesPage.tsx b/admin-web/src/features/policies/PoliciesPage.tsx index a75c5d9..4d70d26 100644 --- a/admin-web/src/features/policies/PoliciesPage.tsx +++ b/admin-web/src/features/policies/PoliciesPage.tsx @@ -9,7 +9,8 @@ const columns = [ { key: "name", label: "Policy" }, { key: "targets", label: "Targets" }, { key: "destinations", label: "Destinations" }, - { key: "mode", label: "Mode" } + { key: "mode", label: "Mode" }, + { key: "actions", label: "Actions" } ]; export function PoliciesPage() { @@ -29,6 +30,14 @@ export function PoliciesPage() { targetUserId: "", fullTunnel: false }); + const [editingPolicyId, setEditingPolicyId] = useState(null); + const [editForm, setEditForm] = useState({ + name: "", + description: "", + destinations: "", + fullTunnel: false, + isActive: true + }); const createMutation = useMutation({ mutationFn: api.createPolicy, @@ -38,9 +47,27 @@ export function PoliciesPage() { } }); + const updateMutation = useMutation({ + mutationFn: ({ policyId, payload }: { policyId: string; payload: { name: string; description: string; destinations: string[]; full_tunnel: boolean; is_active: boolean } }) => + api.updatePolicy(policyId, payload), + onSuccess: () => { + setEditingPolicyId(null); + setEditForm({ name: "", description: "", destinations: "", fullTunnel: false, isActive: true }); + void queryClient.invalidateQueries({ queryKey: ["policies"] }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: api.deletePolicy, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["policies"] }); + } + }); + const rows = query.data?.map((policy) => ({ + id: policy.id, name: policy.name, - targets: "assigned targets", + targets: policy.targets?.length ? `${policy.targets.length} target(s)` : "assigned targets", destinations: policy.destinations?.join(", ") ?? (policy.full_tunnel ? "0.0.0.0/0" : "-"), mode: policy.full_tunnel ? "Full tunnel" : "Split tunnel" })) ?? []; @@ -63,6 +90,38 @@ export function PoliciesPage() { }); } + function startEdit(policyId: string) { + const policy = query.data?.find((item) => item.id === policyId); + if (!policy) { + return; + } + setEditingPolicyId(policyId); + setEditForm({ + name: policy.name, + description: policy.description, + destinations: policy.destinations?.join(", ") ?? "", + fullTunnel: policy.full_tunnel, + isActive: policy.is_active + }); + } + + function onEditSubmit(event: FormEvent) { + event.preventDefault(); + if (!editingPolicyId) { + return; + } + updateMutation.mutate({ + policyId: editingPolicyId, + payload: { + name: editForm.name, + description: editForm.description, + destinations: editForm.fullTunnel ? ["0.0.0.0/0"] : editForm.destinations.split(",").map((value) => value.trim()).filter(Boolean), + full_tunnel: editForm.fullTunnel, + is_active: editForm.isActive + } + }); + } + return ( {query.isError ?

Unable to load policies from the API.

: null} {createMutation.isError ?

Unable to create policy.

: null} + {updateMutation.isError ?

Unable to update policy.

: null} + {deleteMutation.isError ?

Unable to delete policy.

: null} + {editingPolicyId ? ( +
+ setEditForm((value) => ({ ...value, name: event.target.value }))} /> + setEditForm((value) => ({ ...value, description: event.target.value }))} /> + setEditForm((value) => ({ ...value, destinations: event.target.value }))} + disabled={editForm.fullTunnel} + /> + + +
+ + +
+
+ ) : null} {row[column.key as keyof (typeof rows)[number]]}} + renderCell={(row, column) => { + if (column.key === "actions") { + return ( +
+ + +
+ ); + } + return {row[column.key as keyof (typeof rows)[number]]}; + }} /> ); diff --git a/admin-web/src/features/users/UsersPage.tsx b/admin-web/src/features/users/UsersPage.tsx index de40ea6..adad485 100644 --- a/admin-web/src/features/users/UsersPage.tsx +++ b/admin-web/src/features/users/UsersPage.tsx @@ -8,8 +8,10 @@ import { Table } from "../../components/Table"; const columns = [ { key: "username", label: "Username" }, { key: "name", label: "Display name" }, + { key: "email", label: "Email" }, { key: "role", label: "Role" }, - { key: "status", label: "Status" } + { key: "status", label: "Status" }, + { key: "actions", label: "Actions" } ]; export function UsersPage() { @@ -25,6 +27,14 @@ export function UsersPage() { password: "", role: "user" }); + const [editingUserId, setEditingUserId] = useState(null); + const [editForm, setEditForm] = useState({ + display_name: "", + email: "", + role: "user", + password: "", + is_active: true + }); const createMutation = useMutation({ mutationFn: api.createUser, @@ -34,9 +44,27 @@ export function UsersPage() { } }); + const updateMutation = useMutation({ + mutationFn: ({ userId, payload }: { userId: string; payload: typeof editForm }) => api.updateUser(userId, payload), + onSuccess: () => { + setEditingUserId(null); + setEditForm({ display_name: "", email: "", role: "user", password: "", is_active: true }); + void queryClient.invalidateQueries({ queryKey: ["users"] }); + } + }); + + const deleteMutation = useMutation({ + mutationFn: api.deleteUser, + onSuccess: () => { + void queryClient.invalidateQueries({ queryKey: ["users"] }); + } + }); + const rows = query.data?.map((user) => ({ + id: user.id, username: user.username, name: user.display_name, + email: user.email || "-", role: user.role, status: user.is_active ? "active" : "disabled" })) ?? []; @@ -46,6 +74,32 @@ export function UsersPage() { createMutation.mutate(form); } + function startEdit(userId: string) { + const user = query.data?.find((item) => item.id === userId); + if (!user) { + return; + } + setEditingUserId(userId); + setEditForm({ + display_name: user.display_name, + email: user.email || "", + role: user.role, + password: "", + is_active: user.is_active + }); + } + + function onEditSubmit(event: FormEvent) { + event.preventDefault(); + if (!editingUserId) { + return; + } + updateMutation.mutate({ + userId: editingUserId, + payload: editForm + }); + } + return ( {query.isError ?

Unable to load users from the API.

: null} {createMutation.isError ?

Unable to create user.

: null} + {updateMutation.isError ?

Unable to update user.

: null} + {deleteMutation.isError ?

Unable to delete user.

: null} + {editingUserId ? ( +
+ setEditForm((value) => ({ ...value, display_name: event.target.value }))} /> + setEditForm((value) => ({ ...value, email: event.target.value }))} /> + + setEditForm((value) => ({ ...value, password: event.target.value }))} /> + +
+ + +
+ + ) : null}
{row[column.key as keyof (typeof rows)[number]]}} + renderCell={(row, column) => { + if (column.key === "actions") { + return ( +
+ + +
+ ); + } + return {row[column.key as keyof (typeof rows)[number]]}; + }} /> ); diff --git a/backend/internal/device/repository.go b/backend/internal/device/repository.go index 3b79f10..589a816 100644 --- a/backend/internal/device/repository.go +++ b/backend/internal/device/repository.go @@ -94,8 +94,8 @@ func (r *PGRepository) GetLatestEnrollmentByUser(ctx context.Context, userID uui g.name, g.endpoint, g.public_key, - wp.dns_servers, - wp.allowed_ips + coalesce(wp.dns_servers, '{}')::text[], + coalesce(array(select cidr::text from unnest(wp.allowed_ips) as cidr), '{}')::text[] from devices d join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null join gateways g on g.id = wp.gateway_id @@ -121,8 +121,8 @@ func (r *PGRepository) GetEnrollmentByDeviceID(ctx context.Context, deviceID uui g.name, g.endpoint, g.public_key, - wp.dns_servers, - wp.allowed_ips + coalesce(wp.dns_servers, '{}')::text[], + coalesce(array(select cidr::text from unnest(wp.allowed_ips) as cidr), '{}')::text[] from devices d join wireguard_peers wp on wp.device_id = d.id and wp.deleted_at is null join gateways g on g.id = wp.gateway_id diff --git a/backend/internal/httpserver/router.go b/backend/internal/httpserver/router.go index 7c7ce92..eda3a34 100644 --- a/backend/internal/httpserver/router.go +++ b/backend/internal/httpserver/router.go @@ -52,6 +52,8 @@ func NewRouter(jwtSecret string, handlers Handlers) http.Handler { r.Use(AdminOnly) r.Get("/users", handlers.User.List) r.Post("/users", handlers.User.Create) + r.Patch("/users/{id}", handlers.User.Update) + r.Delete("/users/{id}", handlers.User.Delete) r.Post("/users/{id}/disable", handlers.User.Disable) r.Post("/users/{id}/enable", handlers.User.Enable) r.Get("/devices", handlers.Device.ListAll) @@ -60,6 +62,8 @@ func NewRouter(jwtSecret string, handlers Handlers) http.Handler { r.Post("/devices/{id}/rotate", handlers.Device.Rotate) r.Get("/policies", handlers.Policy.List) r.Post("/policies", handlers.Policy.Create) + r.Patch("/policies/{id}", handlers.Policy.Update) + r.Delete("/policies/{id}", handlers.Policy.Delete) r.Get("/gateways", handlers.Gateway.List) r.Get("/gateways/{id}/sync", handlers.Gateway.SyncBundle) r.Patch("/gateways/{id}", handlers.Gateway.Update) diff --git a/backend/internal/policy/handler.go b/backend/internal/policy/handler.go index cac9861..12390d2 100644 --- a/backend/internal/policy/handler.go +++ b/backend/internal/policy/handler.go @@ -4,6 +4,9 @@ import ( "encoding/json" "net/http" + "github.com/go-chi/chi/v5" + "github.com/google/uuid" + "nexavpn/backend/internal/apiutil" "nexavpn/backend/internal/audit" "nexavpn/backend/internal/requestctx" @@ -60,3 +63,58 @@ func (h *Handler) Create(w http.ResponseWriter, r *http.Request) { }) apiutil.JSON(w, http.StatusCreated, item) } + +func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { + policyID, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_policy_id", "invalid policy id") + return + } + + var input UpdateRequest + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body") + return + } + + item, err := h.service.Update(r.Context(), policyID, input) + if err != nil { + apiutil.Error(w, http.StatusInternalServerError, "policy_update_failed", "unable to update policy") + return + } + + if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok { + _ = h.audit.Record(r.Context(), audit.Entry{ + ActorUserID: &claims.UserID, + EntityType: "policy", + EntityID: &policyID, + EventType: "admin.policy.updated", + Status: "success", + Message: "admin updated policy", + }) + } + apiutil.JSON(w, http.StatusOK, item) +} + +func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { + policyID, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_policy_id", "invalid policy id") + return + } + if err := h.service.Delete(r.Context(), policyID); err != nil { + apiutil.Error(w, http.StatusInternalServerError, "policy_delete_failed", "unable to delete policy") + return + } + if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok { + _ = h.audit.Record(r.Context(), audit.Entry{ + ActorUserID: &claims.UserID, + EntityType: "policy", + EntityID: &policyID, + EventType: "admin.policy.deleted", + Status: "success", + Message: "admin deleted policy", + }) + } + apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true}) +} diff --git a/backend/internal/policy/repository.go b/backend/internal/policy/repository.go index eaae8a6..40ab426 100644 --- a/backend/internal/policy/repository.go +++ b/backend/internal/policy/repository.go @@ -2,6 +2,7 @@ package policy import ( "context" + "errors" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" @@ -10,6 +11,8 @@ import ( type Repository interface { List(ctx context.Context) ([]Policy, error) Create(ctx context.Context, input CreateRequest, createdBy uuid.UUID) (Policy, error) + Update(ctx context.Context, policyID uuid.UUID, input UpdateRequest) (Policy, error) + Delete(ctx context.Context, policyID uuid.UUID) error ResolveDestinations(ctx context.Context, userID uuid.UUID, deviceID *uuid.UUID) ([]string, error) } @@ -106,6 +109,81 @@ func (r *PGRepository) Create(ctx context.Context, input CreateRequest, createdB return inputPolicy, nil } +func (r *PGRepository) Update(ctx context.Context, policyID uuid.UUID, input UpdateRequest) (Policy, error) { + tx, err := r.db.Begin(ctx) + if err != nil { + return Policy{}, err + } + defer tx.Rollback(ctx) + + _, err = tx.Exec(ctx, ` + update policies + set + name = coalesce($2, name), + description = coalesce($3, description), + priority = coalesce($4, priority), + effect = coalesce($5, effect), + full_tunnel = coalesce($6, full_tunnel), + is_active = coalesce($7, is_active), + updated_at = now() + where id = $1 and deleted_at is null + `, policyID, input.Name, input.Description, input.Priority, input.Effect, input.FullTunnel, input.IsActive) + if err != nil { + return Policy{}, err + } + + if input.Destinations != nil { + if _, err := tx.Exec(ctx, `delete from policy_destinations where policy_id = $1`, policyID); err != nil { + return Policy{}, err + } + for _, destination := range input.Destinations { + if _, err := tx.Exec(ctx, ` + insert into policy_destinations (id, policy_id, destination) + values ($1, $2, $3::cidr) + `, uuid.New(), policyID, destination); err != nil { + return Policy{}, err + } + } + } + + if input.Targets != nil { + if _, err := tx.Exec(ctx, `delete from policy_targets where policy_id = $1`, policyID); err != nil { + return Policy{}, err + } + for _, target := range input.Targets { + if _, err := tx.Exec(ctx, ` + insert into policy_targets (id, policy_id, target_type, target_id) + values ($1, $2, $3, $4) + `, uuid.New(), policyID, target.Type, target.ID); err != nil { + return Policy{}, err + } + } + } + + if err := tx.Commit(ctx); err != nil { + return Policy{}, err + } + + items, err := r.List(ctx) + if err != nil { + return Policy{}, err + } + for _, item := range items { + if item.ID == policyID { + if input.Targets != nil { + item.Targets = input.Targets + } + return item, nil + } + } + return Policy{}, errors.New("policy not found after update") +} + +func (r *PGRepository) Delete(ctx context.Context, policyID uuid.UUID) error { + _, err := r.db.Exec(ctx, `update policies set deleted_at = now(), updated_at = now() where id = $1 and deleted_at is null`, policyID) + return err +} + func (r *PGRepository) ResolveDestinations(ctx context.Context, userID uuid.UUID, deviceID *uuid.UUID) ([]string, error) { query := ` select distinct pd.destination::text diff --git a/backend/internal/policy/service.go b/backend/internal/policy/service.go index 0c5df8b..e1fac9f 100644 --- a/backend/internal/policy/service.go +++ b/backend/internal/policy/service.go @@ -28,6 +28,14 @@ func (s *Service) Create(ctx context.Context, actorID uuid.UUID, input CreateReq return s.repo.Create(ctx, input, actorID) } +func (s *Service) Update(ctx context.Context, policyID uuid.UUID, input UpdateRequest) (Policy, error) { + return s.repo.Update(ctx, policyID, input) +} + +func (s *Service) Delete(ctx context.Context, policyID uuid.UUID) error { + return s.repo.Delete(ctx, policyID) +} + func (s *Service) ResolveDestinations(ctx context.Context, userID uuid.UUID, deviceID *uuid.UUID) ([]string, error) { return s.repo.ResolveDestinations(ctx, userID, deviceID) } diff --git a/backend/internal/policy/types.go b/backend/internal/policy/types.go index 49d5b91..cf98c54 100644 --- a/backend/internal/policy/types.go +++ b/backend/internal/policy/types.go @@ -28,3 +28,14 @@ type CreateRequest struct { Destinations []string `json:"destinations"` Targets []Target `json:"targets"` } + +type UpdateRequest struct { + Name *string `json:"name"` + Description *string `json:"description"` + Priority *int `json:"priority"` + Effect *string `json:"effect"` + FullTunnel *bool `json:"full_tunnel"` + IsActive *bool `json:"is_active"` + Destinations []string `json:"destinations"` + Targets []Target `json:"targets"` +} diff --git a/backend/internal/user/handler.go b/backend/internal/user/handler.go index 5b52897..b084bce 100644 --- a/backend/internal/user/handler.go +++ b/backend/internal/user/handler.go @@ -86,6 +86,61 @@ func (h *Handler) Disable(w http.ResponseWriter, r *http.Request) { apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true}) } +func (h *Handler) Update(w http.ResponseWriter, r *http.Request) { + targetID, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_user_id", "invalid user id") + return + } + + var input UpdateRequest + if err := json.NewDecoder(r.Body).Decode(&input); err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_json", "invalid request body") + return + } + + updated, err := h.service.Update(r.Context(), targetID.String(), input) + if err != nil { + apiutil.Error(w, http.StatusInternalServerError, "user_update_failed", "unable to update user") + return + } + + if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok { + _ = h.audit.Record(r.Context(), audit.Entry{ + ActorUserID: &claims.UserID, + EntityType: "user", + EntityID: &targetID, + EventType: "admin.user.updated", + Status: "success", + Message: "admin updated user", + }) + } + apiutil.JSON(w, http.StatusOK, updated) +} + +func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) { + targetID, err := uuid.Parse(chi.URLParam(r, "id")) + if err != nil { + apiutil.Error(w, http.StatusBadRequest, "invalid_user_id", "invalid user id") + return + } + if err := h.service.Delete(r.Context(), targetID.String()); err != nil { + apiutil.Error(w, http.StatusInternalServerError, "user_delete_failed", "unable to delete user") + return + } + if claims, ok := requestctx.ClaimsFromContext(r.Context()); ok { + _ = h.audit.Record(r.Context(), audit.Entry{ + ActorUserID: &claims.UserID, + EntityType: "user", + EntityID: &targetID, + EventType: "admin.user.deleted", + Status: "success", + Message: "admin deleted user", + }) + } + apiutil.JSON(w, http.StatusOK, map[string]any{"ok": true}) +} + func (h *Handler) Enable(w http.ResponseWriter, r *http.Request) { targetID, err := uuid.Parse(chi.URLParam(r, "id")) if err != nil { diff --git a/backend/internal/user/repository.go b/backend/internal/user/repository.go index fffbe9e..db0259e 100644 --- a/backend/internal/user/repository.go +++ b/backend/internal/user/repository.go @@ -10,6 +10,8 @@ import ( type Repository interface { List(ctx context.Context) ([]User, error) Create(ctx context.Context, input CreateRequest, passwordHash string) (User, error) + Update(ctx context.Context, userID uuid.UUID, input UpdateRequest, passwordHash *string) (User, error) + Delete(ctx context.Context, userID uuid.UUID) error SetActive(ctx context.Context, userID uuid.UUID, active bool) error } @@ -69,3 +71,35 @@ func (r *PGRepository) SetActive(ctx context.Context, userID uuid.UUID, active b _, err := r.db.Exec(ctx, `update users set is_active = $2, updated_at = now() where id = $1`, userID, active) return err } + +func (r *PGRepository) Update(ctx context.Context, userID uuid.UUID, input UpdateRequest, passwordHash *string) (User, error) { + const query = ` + update users u + set + role_id = coalesce((select id from roles where name = $2), u.role_id), + display_name = coalesce($3, u.display_name), + email = case when $4 is null then u.email else nullif($4, '')::citext end, + password_hash = coalesce($5, u.password_hash), + is_active = coalesce($6, u.is_active), + updated_at = now() + where u.id = $1 and u.deleted_at is null + returning + u.id, + u.role_id, + (select name from roles where id = u.role_id), + u.username::text, + u.display_name, + coalesce(u.email::text, ''), + u.is_active + ` + + var item User + err := r.db.QueryRow(ctx, query, userID, input.Role, input.DisplayName, input.Email, passwordHash, input.IsActive). + Scan(&item.ID, &item.RoleID, &item.RoleName, &item.Username, &item.DisplayName, &item.Email, &item.IsActive) + return item, err +} + +func (r *PGRepository) Delete(ctx context.Context, userID uuid.UUID) error { + _, err := r.db.Exec(ctx, `update users set deleted_at = now(), updated_at = now() where id = $1 and deleted_at is null`, userID) + return err +} diff --git a/backend/internal/user/service.go b/backend/internal/user/service.go index 8de3c55..52187bc 100644 --- a/backend/internal/user/service.go +++ b/backend/internal/user/service.go @@ -28,6 +28,32 @@ func (s *Service) Create(ctx context.Context, input CreateRequest) (User, error) return s.repo.Create(ctx, input, passwordHash) } +func (s *Service) Update(ctx context.Context, userID string, input UpdateRequest) (User, error) { + id, err := uuid.Parse(userID) + if err != nil { + return User{}, err + } + + var passwordHash *string + if input.Password != nil && *input.Password != "" { + hashed, err := auth.HashPassword(*input.Password) + if err != nil { + return User{}, err + } + passwordHash = &hashed + } + + return s.repo.Update(ctx, id, input, passwordHash) +} + +func (s *Service) Delete(ctx context.Context, userID string) error { + id, err := uuid.Parse(userID) + if err != nil { + return err + } + return s.repo.Delete(ctx, id) +} + func (s *Service) SetActive(ctx context.Context, userID string, active bool) error { id, err := uuid.Parse(userID) if err != nil { diff --git a/backend/internal/user/types.go b/backend/internal/user/types.go index c779b1b..30c178e 100644 --- a/backend/internal/user/types.go +++ b/backend/internal/user/types.go @@ -21,7 +21,9 @@ type CreateRequest struct { } type UpdateRequest struct { + Role *string `json:"role"` DisplayName *string `json:"display_name"` Email *string `json:"email"` + Password *string `json:"password"` IsActive *bool `json:"is_active"` } diff --git a/desktop-client/src-tauri/src/lib.rs b/desktop-client/src-tauri/src/lib.rs index b25670b..90bdd28 100644 --- a/desktop-client/src-tauri/src/lib.rs +++ b/desktop-client/src-tauri/src/lib.rs @@ -2,7 +2,7 @@ mod tunnel_manager; use std::{fs, path::PathBuf, sync::Mutex}; -use base64::{engine::general_purpose::STANDARD_NO_PAD, Engine as _}; +use base64::{engine::general_purpose::STANDARD, Engine as _}; use rand_core::OsRng; use reqwest::Client; use serde::{Deserialize, Serialize}; @@ -225,8 +225,8 @@ fn generate_keypair() -> (String, String) { let private = StaticSecret::random_from_rng(OsRng); let public = PublicKey::from(&private); ( - STANDARD_NO_PAD.encode(private.to_bytes()), - STANDARD_NO_PAD.encode(public.to_bytes()), + STANDARD.encode(private.to_bytes()), + STANDARD.encode(public.to_bytes()), ) }