Compare commits

..
11 Commits
Author SHA1 Message Date
Misha Bragin b06cb0ec3d Add a settings page for custom IdP and MFA (#308) 2023-12-12 13:42:14 +01:00
Misha Bragin 0c924f7ded Correct peer approval tooltip (#307) 2023-12-12 13:33:11 +01:00
Maycon Santos f29c915fc2 fix settings update blank page 2023-12-11 18:07:05 +01:00
Maycon Santos 5f8579bfda Fix peer expiration interval init on validation (#306) 2023-12-11 17:46:32 +01:00
pascal-fischer a71389aa29 not sending extra settings on selfhosted deployments (#305) 2023-12-11 16:52:29 +01:00
Maycon Santos c3ba026452 Init domains array for custom nameserver group (#304) 2023-12-08 11:07:10 +01:00
Maycon Santos 193f8a7bdf Fix missing text update from #302 (#303) 2023-12-07 18:05:44 +01:00
Fabio Fantoni f9814e1169 add codespell github workflow and fix some typo (#296)
* add codespell github workflow

* fix some typo
2023-12-07 18:05:31 +01:00
Maycon Santos 2f800bf912 Add delete account support (#302)
* Add delete account support

* add peer approval form item

* block menu for non owners
2023-12-07 15:28:07 +01:00
pascal-fischerandMaycon Santos 0199ea81f3 Add support for peer approval (#299)
* add support for peer approval

* use gold color for tag

---------

Co-authored-by: Maycon Santos <mlsmaycon@gmail.com>
2023-12-05 13:17:40 +01:00
Maycon Santos a20894092b Add owner role support (#300)
Update checks for admin to include owner role

Updated role list to include role for users

Updated activity with new event type handler
2023-12-01 16:57:18 +01:00
19 changed files with 867 additions and 271 deletions

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
name: Codespell
on: [pull_request]
jobs:
check:
name: Check
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: codespell
uses: codespell-project/actions-codespell@v2
with:
only_warn: 1
skip: package-lock.json,*.svg
+3 -3
View File
@@ -1,15 +1,15 @@
# Wiretrustee Dashboard
Wiretrustee Dashboard is a the Wiretrustee Managemenet server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
Wiretrustee Dashboard is a the Wiretrustee Management server UI. It allow users to signin, view setup keys and manage peers. This image is **not ready** for production use.
## Tags
```latest``` ```vX.X.X``` not available yet.
```main``` builded on every PR being merged to the repository
```main``` built on every PR being merged to the repository
## How to use this image
HTTP run:
```shell
docker run -d --rm -p 80:80 wiretrustee/dashboard:main
```
Using SSL certificate from Let's Encript®:
Using SSL certificate from Let's Encrypt®:
```shell
docker run -d --rm -p 80:80 -p 443:443 \
-e LETSENCRYPT_DOMAIN=app.mydomain.com \
+1 -1
View File
@@ -387,7 +387,7 @@ check_nb_domain() {
if [ "$DOMAIN" == "netbird.example.com" ]; then
echo "The NETBIRD_DOMAIN cannot be netbird.example.com" > /dev/stderr
retrun 1
return 1
fi
return 0
}
+1 -1
View File
@@ -17,7 +17,7 @@ export class AccessControlPage {
}
async assertDefaultAccessCotrolIsCreated() {
await test.step('Assert that default cotrol access is created', async () => {
await test.step('Assert that default control access is created', async () => {
await expect(this.defaulAccessControl).toBeVisible();
})
}
+2
View File
@@ -126,6 +126,7 @@ const NameServerGroupAdd = () => {
};
const onChange = (changedValues: any) => {
if (changedValues.primary !== undefined) {
setIsPrimary(changedValues.primary);
}
@@ -207,6 +208,7 @@ const NameServerGroupAdd = () => {
let nsGroupLocal = {} as NameServerGroup;
if (value === customChoice) {
nsGroupLocal = nsGroup;
nsGroupLocal.domains = [];
} else {
defaultDNSOptions.forEach((nsg) => {
if (value === nsg.name) {
+1 -1
View File
@@ -161,7 +161,7 @@ const Navbar = () => {
return false;
}
if (user.role?.toLowerCase() === "admin") {
if (user.role?.toLowerCase() === "admin" || user.role?.toLowerCase() === "owner") {
return true;
}
return !adminOnlyTabs.find((t) => t === key);
+23 -10
View File
@@ -79,6 +79,7 @@ const UserEdit = (props: any) => {
const [formUser, setFormUser] = useState({} as FormUser);
const [form] = Form.useForm();
const [isAdmin, setIsAdmin] = useState(false);
const [isOwner, setIsOwner] = useState(false);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
@@ -94,15 +95,21 @@ const UserEdit = (props: any) => {
};
const createUserToSave = (values: any): UserToSave => {
const autoGroups =
groups
?.filter((g) => values.autoGroupsNames.includes(g.id))
.map((g) => g.id || "") || [];
let autoGroups:string[] = []
if (values.autoGroupsNames) {
autoGroups =
groups
?.filter((g) => values.autoGroupsNames.includes(g.id))
.map((g) => g.id || "") || [];
}
// find groups that do not yet exist (newly added by the user)
const allGroupsNames: string[] = groups?.map((g) => g.id || "");
const groupsToCreate = values.autoGroupsNames.filter(
(s: string) => !allGroupsNames.includes(s)
);
let groupsToCreate:string[] = []
if (values.autoGroupsNames) {
groupsToCreate = values.autoGroupsNames.filter(
(s: string) => !allGroupsNames.includes(s)
);
}
let userID = user ? user.id : "";
let isServiceUser = user ? user?.is_service_user : false;
return {
@@ -120,7 +127,8 @@ const UserEdit = (props: any) => {
if (users) {
let currentUser = users.find((user) => user?.is_current);
if (currentUser) {
setIsAdmin(currentUser.role === "admin");
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
setIsOwner(currentUser.role === "owner");
}
}
}, [users]);
@@ -403,7 +411,7 @@ const UserEdit = (props: any) => {
>
<Select
style={{ width: "100%" }}
disabled={user?.is_current}
disabled={user?.is_current || user?.role === "owner"}
>
<Option value="admin">
<Text type={"secondary"}>admin</Text>
@@ -411,6 +419,11 @@ const UserEdit = (props: any) => {
<Option value="user">
<Text type={"secondary"}>user</Text>
</Option>
{!user?.is_service_user && isOwner && (
<Option value="owner">
<Text type={"secondary"}>owner</Text>
</Option>
)}
</Select>
</Form.Item>
</Col>
@@ -473,7 +486,7 @@ const UserEdit = (props: any) => {
label="Block user"
style={{ marginRight: "50px", fontWeight: "500" }}
>
<Switch />
<Switch disabled={user?.role == "owner"} />
</Form.Item>
</Col>
)}
+8 -1
View File
@@ -1,6 +1,6 @@
import {ActionType, createAction, createAsyncAction} from 'typesafe-actions';
import {Account} from './types';
import {ApiError, ChangeResponse, RequestPayload} from '../../services/api-client/types';
import {ApiError, ChangeResponse, DeleteResponse, RequestPayload} from '../../services/api-client/types';
const actions = {
getAccounts: createAsyncAction(
@@ -16,6 +16,13 @@ const actions = {
)<RequestPayload<Account>, ChangeResponse<Account | null>, ChangeResponse<Account | null>>(),
setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')<ChangeResponse<Account | null>>(),
resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')<null>(),
deleteAccount: createAsyncAction(
'DELETE_ACCOUNT_REQUEST',
'DELETE_ACCOUNT_SUCCESS',
'DELETE_ACCOUNT_FAILURE'
)<RequestPayload<string>, DeleteResponse<string | null>, DeleteResponse<string | null>>(),
resetDeletedAccount: createAction('RESET_DELETED_ACCOUNT')<null>(),
setDeleteAccount: createAction('SET_DELETE_ACCOUNT')<DeleteResponse<string | null>>(),
};
+18 -2
View File
@@ -2,13 +2,14 @@ import { createReducer } from 'typesafe-actions';
import { combineReducers } from 'redux';
import { Account } from './types';
import actions, { ActionTypes } from './actions';
import {ApiError, ChangeResponse} from "../../services/api-client/types";
import {ApiError, ChangeResponse, DeleteResponse} from "../../services/api-client/types";
type StateType = Readonly<{
data: Account[] | null;
loading: boolean;
failed: ApiError | null;
savedAccount: ChangeResponse<Account | null>;
deletedAccount: DeleteResponse<string | null>;
}>;
const initialState: StateType = {
@@ -22,6 +23,13 @@ const initialState: StateType = {
error: null,
data : null
},
deletedAccount: <DeleteResponse<string | null>>{
loading: false,
success: false,
failure: false,
error: null,
data : null
},
};
const data = createReducer<Account[], ActionTypes>(initialState.data as Account[])
@@ -45,10 +53,18 @@ const updatedAccount = createReducer<ChangeResponse<Account | null>, ActionTypes
.handleAction(actions.setUpdateAccount, (store, action) => action.payload)
.handleAction(actions.resetUpdateAccount, () => initialState.savedAccount)
const deleteAccount = createReducer<DeleteResponse<string | null>, ActionTypes>(initialState.deletedAccount)
.handleAction(actions.deleteAccount.request, () => initialState.deletedAccount)
.handleAction(actions.deleteAccount.success, (store, action) => action.payload)
.handleAction(actions.deleteAccount.failure, (store, action) => action.payload)
.handleAction(actions.setDeleteAccount, (store, action) => action.payload)
.handleAction(actions.resetDeletedAccount, () => initialState.deletedAccount)
export default combineReducers({
data,
loading,
failed,
updatedAccount
updatedAccount,
deleteAccount
});
+48 -2
View File
@@ -1,8 +1,9 @@
import {all, call, put, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse} from '../../services/api-client/types';
import {all, call, put, select, takeLatest} from 'redux-saga/effects';
import {ApiError, ApiResponse, ChangeResponse, DeleteResponse} from '../../services/api-client/types';
import {Account} from './types'
import service from './service';
import actions from './actions';
import {deletePeer} from "../peer/sagas";
export function* getAccounts(action: ReturnType<typeof actions.getAccounts.request>): Generator {
try {
@@ -55,10 +56,55 @@ export function* updateAccount(action: ReturnType<typeof actions.updateAccount.r
}
}
export function* deleteAccount(
action: ReturnType<typeof actions.deleteAccount.request>
): Generator {
try {
yield call(actions.setDeleteAccount, {
loading: true,
success: false,
failure: false,
error: null,
data: null,
} as DeleteResponse<string | null>);
const effect = yield call(service.deleteAccount, action.payload);
const response = effect as ApiResponse<any>;
yield put(
actions.deleteAccount.success({
loading: false,
success: true,
failure: false,
error: null,
data: response.body,
} as DeleteResponse<string | null>)
);
const accounts = (yield select((state) => state.peer.data)) as Account[];
yield put(
actions.getAccounts.success(
accounts.filter((p: Account) => p.id !== action.payload.payload)
)
);
} catch (err) {
yield put(
actions.deleteAccount.failure({
loading: false,
success: false,
failure: false,
error: err as ApiError,
data: null,
} as DeleteResponse<string | null>)
);
}
}
export default function* sagas(): Generator {
yield all([
takeLatest(actions.getAccounts.request, getAccounts),
takeLatest(actions.updateAccount.request, updateAccount),
takeLatest(actions.deleteAccount.request, deleteAccount),
]);
}
+7 -1
View File
@@ -15,5 +15,11 @@ export default {
`/api/accounts/${id}`,
payload
);
}
},
async deleteAccount(payload:RequestPayload<string>): Promise<ApiResponse<any>> {
return apiClient.delete<any>(
`/api/accounts/` + payload.payload,
payload
);
},
};
+4
View File
@@ -8,6 +8,9 @@ export interface Account {
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
extra: {
peer_approval_enabled: boolean;
}
};
}
@@ -17,4 +20,5 @@ export interface FormAccount extends Account {
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
peer_login_expiration_formatted: ExpiresInValue;
peer_approval_enabled: boolean;
}
+2 -1
View File
@@ -16,7 +16,8 @@ export interface Peer {
dns_label: string,
last_login: string,
login_expired: boolean,
login_expiration_enabled: boolean
login_expiration_enabled: boolean,
approval_required: boolean
}
export interface FormPeer extends Peer {
+1 -1
View File
@@ -96,7 +96,7 @@ export const transformGroupedDataTable = (
}
});
groupList = groupList.filter(
(value, index, arrary) => arrary.indexOf(value) === index
(value, index, array) => array.indexOf(value) === index
);
let groupDataTableRoutes = transformDataTable(listedRoutes, peers);
const filterEnabledRoutes = groupDataTableRoutes.filter(
+18
View File
@@ -338,6 +338,8 @@ export const Activity = () => {
case "peer.login.expiration.enable":
case "user.peer.login":
case "peer.login.expire":
case "peer.approve":
case "peer.approval.revoke":
return renderMultiRowSpan(event.meta.fqdn, event.meta.ip);
case "route.add":
case "route.delete":
@@ -362,6 +364,20 @@ export const Activity = () => {
);
}
return "-";
case "transferred.owner.role":
if (event.meta.email || event.meta.username || event.target_id) {
return renderMultiRowSpan(
event.meta.username ? event.meta.username : event.target_id,
event.meta.email ? event.meta.email : "User"
);
}
if (event.meta.user_name) {
return renderMultiRowSpan(
event.meta.user_name,
event.meta.is_service_user ? "Service User" : "User"
);
}
return "-";
case "setupkey.group.add":
case "setupkey.group.delete":
return renderMultiRowSpan(event.meta.setupkey, "Setup Key");
@@ -373,6 +389,8 @@ export const Activity = () => {
case "account.setting.peer.login.expiration.enable":
case "account.setting.peer.login.expiration.disable":
case "account.setting.peer.login.expiration.update":
case "account.setting.peer.approval.enable":
case "account.setting.peer.approval.disable":
case "integration.create":
case "integration.update":
case "integration.delete":
+68 -6
View File
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from "react";
import { capitalize, formatOS, timeAgo } from "../utils/common";
import {capitalize, formatOS, isLocalDev, isNetBirdHosted, timeAgo} from "../utils/common";
import { useDispatch, useSelector } from "react-redux";
import { RootState } from "typesafe-actions";
import { actions as peerActions } from "../store/peer";
@@ -79,6 +79,7 @@ export const Peers = () => {
(state: RootState) => state.peer.updateGroupsVisible
);
const users = useSelector((state: RootState) => state.user.data);
const account = useSelector((state: RootState) => state.account.data);
const [addPeerModalOpen, setAddPeerModalOpen] = useState(false);
const { oidcUser } = useOidcUser();
const [isAdmin, setIsAdmin] = useState(false);
@@ -100,6 +101,7 @@ export const Peers = () => {
const optionsOnOff = [
{ label: "Online", value: "on" },
{ label: "All", value: "all" },
// ...((isNetBirdHosted() || isLocalDev()) && account[0].settings.extra.peer_approval_enabled ? [{ label: "Needs approval", value: "approval" }] : []),
];
const transformDataTable = (d: Peer[]): PeerDataTable[] => {
@@ -126,7 +128,7 @@ export const Peers = () => {
if (users) {
let currentUser = users.find((user) => user.is_current);
if (currentUser) {
setIsAdmin(currentUser.role === "admin");
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
}
}
}, [users]);
@@ -291,6 +293,16 @@ export const Peers = () => {
);
}) as Peer[];
// switch (optionOnOff) {
// case "on":
// f = filter(f, (f: Peer) => f.connected);
// break;
// case "approval":
// f = filter(f, (f: Peer) => f.approval_required);
// break;
// default:
// break;
// }
if (optionOnOff === "on") {
f = filter(f, (f: Peer) => f.connected);
}
@@ -410,6 +422,36 @@ export const Peers = () => {
});
};
const showConfirmApprove = (record: PeerDataTable) => {
setPeerToAction(record);
let content = (
<Paragraph>
Are you sure you want to approve this peer?
</Paragraph>
);
let name = record ? record.name : "";
confirmModal.confirm({
icon: <ExclamationCircleOutlined />,
title: <span className="font-500">Approve peer {name}</span>,
width: 600,
content: content,
onOk() {
record.approval_required = false
dispatch(
peerActions.updatePeer.request({
getAccessTokenSilently: getTokenSilently,
payload: record,
})
);
},
onCancel() {
setPeerToAction(null);
},
});
};
const showConfirmEnableSSH = (record: PeerDataTable) => {
confirmModal.confirm({
icon: <ExclamationCircleOutlined />,
@@ -601,6 +643,19 @@ export const Peers = () => {
""
);
let approval = peer.approval_required ? (
<Tooltip title="The peer needs to be approved by an administrator before it can connect to other peers">
<Tag color="gold">
<Text
style={{ fontSize: "10px", color: "rgba(212, 136, 6, 1)" }}
type={"secondary"}
>needs approval</Text>
</Tag>
</Tooltip>
) : (
""
);
const userEmail = users?.find((u) => u.id === peer.user_id)?.email;
let expiry = !peer.login_expiration_enabled ? (
<Tag>
@@ -622,9 +677,9 @@ export const Peers = () => {
<Row>
<Text className="font-500"> {status}</Text>
</Row>
<Row>{loginExpire}</Row>
</span>
</Button>
<Row>{loginExpire}</Row>
</>
);
}
@@ -643,11 +698,18 @@ export const Peers = () => {
<Row>
<Text type="secondary">{userEmail}</Text>
</Row>
<Row style={{ minWidth: "195px" }}>
{expiry} {loginExpire}
</Row>
</span>
</Button>
<Row style={{ minWidth: "195px", paddingLeft: "15px" }}>
{expiry} {loginExpire} <Button
type="text"
style={{ height: "auto", whiteSpace: "normal", textAlign: "center", padding: "0px", margin: "0px" }}
onClick={() => showConfirmApprove(peer)}
className={!isAdmin ? "nohover" : ""}
>
{approval}
</Button>
</Row>
</div>
);
};
+14 -6
View File
@@ -262,7 +262,7 @@ export const RegularUsers = () => {
if (users) {
let currentUser = users.find((user) => user.is_current);
if (currentUser) {
setIsAdmin(currentUser.role === "admin");
setIsAdmin(currentUser.role === "admin" || currentUser.role === "owner");
}
}
}, [users]);
@@ -758,16 +758,20 @@ export const RegularUsers = () => {
<Switch
size={"small"}
checked={e}
disabled={record.is_current}
disabled={record.is_current || record.role === "owner"}
onClick={(active: boolean) => {
handleBlockUser(active, record);
}}
/>
);
if (record.is_current) {
if (record.is_current || record.role === "owner") {
let title = "You can't block or unblock yourself"
if(record.role === "owner") {
title = "You can't block or unblock owners"
}
return (
<Tooltip title="You can't block or unblock yourself">
<Tooltip title={title}>
<Empty
image={""}
description={""}
@@ -803,9 +807,13 @@ export const RegularUsers = () => {
</Button>
);
if (record.is_current) {
if (record.is_current || record.role === "owner") {
let title = "You can't delete yourself"
if(record.role === "owner") {
title = "You can't delete owners"
}
return (
<Tooltip title="You can't delete yourself">
<Tooltip title={title}>
<Empty
image={""}
description={""}
+632 -234
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -52,7 +52,7 @@ export const Users = () => {
if(users) {
let currentUser = users.find((user) => user.is_current)
if(currentUser) {
setIsAdmin(currentUser.role === 'admin');
setIsAdmin(currentUser.role === 'admin' || currentUser.role === 'owner');
}
}
}, [users])