Compare commits

...
6 Commits
10 changed files with 330 additions and 14 deletions

No files matched your search

+14
View File
@@ -6,6 +6,7 @@ export class AddPeerModal {
private readonly windowsTab = this.page.getByTestId('add-peer-modal-windows-tab')
private readonly macTab = this.page.getByTestId('add-peer-modal-mac-tab')
private readonly androidTab = this.page.getByTestId('add-peer-modal-android-tab')
private readonly iosTab = this.page.getByTestId('add-peer-modal-ios-tab')
private readonly dockerTab = this.page.getByTestId('add-peer-modal-docker-tab')
private readonly linuxTabText = this.page.locator('pre').filter({ hasText: 'curl -fsSL https://pkgs.netbird.io/install.sh | sh' })
private readonly windowsDownloadButton = this.page.getByTestId('download-windows-button')
@@ -57,6 +58,13 @@ export class AddPeerModal {
})
}
async openIOSTab() {
await test.step('Open iOS tab on add peer modal', async () => {
await this.iosTab.click();
})
}
async openDockerTab() {
await test.step('Open Docker tab on add peer modal', async () => {
await this.dockerTab.click();
@@ -94,6 +102,12 @@ export class AddPeerModal {
})
}
async assertiOSDownloadButtonHasCorrectLink() {
await test.step('Assert iOS download button has a correct link', async () => {
await expect(this.androidDownloadButton).toHaveAttribute('href', 'https://apps.apple.com/app/netbird-p2p-vpn/id6469329339');
})
}
async assertDockerDownloadButtonHasCorrectLink() {
await test.step('Assert Docker download button has a correct link', async () => {
await expect(this.dockerDownloadButton).toHaveAttribute('href', 'https://docs.docker.com/engine/install/');
+5
View File
@@ -30,6 +30,11 @@ test('Test MacOS tab on a first access add peer modal / @bc', async () => {
await addPeerModal.assertM1M2DownloadButtonHasCorrectLink();
});
test('Test iOS tab on a first access add peer modal', async () => {
await addPeerModal.openIOSTab();
await addPeerModal.assertiOSDownloadButtonHasCorrectLink();
});
test('Test Android tab on a first access add peer modal', async () => {
await addPeerModal.openAndroidTab();
await addPeerModal.assertAndroidDownloadButtonHasCorrectLink();
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

+4 -4
View File
@@ -15,18 +15,18 @@ const Banner = () => {
localStorage.setItem(banner_closed_key,'true');
};
const announcement = "New Release! Manage DNS with NetBird."
const announcement = "NetBird is now available on the App Store."
const announcement_md5 = Md5.hashStr(announcement)
const linkLearnMore = () => {
return (
<a
href="https://docs.netbird.io/how-to/manage-dns-in-your-network"
href="https://apps.apple.com/us/app/netbird-p2p-vpn/id6469329339"
className="font-bold underline"
target="_blank"
rel="noreferrer"
><Text strong style={{color: "#ffffff"}}>Learn more&nbsp;<span aria-hidden="true">&rarr;</span></Text></a>
><Text strong style={{color: "#ffffff"}}>Download&nbsp;<span aria-hidden="true">&rarr;</span></Text></a>
)
}
@@ -36,7 +36,7 @@ const Banner = () => {
if((!stored_banner_closed || stored_banner_closed !== 'true') ||
(!store_banner_md5 || store_banner_md5 !== announcement_md5)) {
//setShow(true);
setShow(true);
localStorage.setItem(banner_md5_key,announcement_md5);
localStorage.setItem(banner_closed_key,'false');
}
@@ -11,6 +11,8 @@ import MacTab from "./MacTab";
import Link from "antd/lib/typography/Link";
import DockerTab from "./DockerTab";
import AndroidTab from "./AndroidTab";
import IOSTab from "./IOSTab";
import {IOSIcon} from "./IOSIcon";
type Props = {
greeting?: string;
@@ -54,11 +56,16 @@ export const AddPeerPopup: React.FC<Props> = ({
},
{
key: "4",
label: <span data-testid="add-peer-modal-ios-tab"><IOSIcon/>iOS</span>,
children: <IOSTab/>,
},
{
key: "5",
label: <span data-testid="add-peer-modal-android-tab"><AndroidFilled/>Android</span>,
children: <AndroidTab/>,
},
{
key: "5",
key: "6",
label: <span data-testid="add-peer-modal-docker-tab"><Icon component={DockerSVG}/>Docker</span>,
children: <DockerTab/>,
}
@@ -0,0 +1,21 @@
import React from 'react';
import Icon, { HomeOutlined } from '@ant-design/icons';
import type { CustomIconComponentProps } from '@ant-design/icons/lib/components/Icon';
import { Space } from 'antd';
const SvgIcon = () => (
<svg xmlns="http://www.w3.org/2000/svg"
width="15"
height="15"
viewBox="0 0 30 30"
>
<path
d="M 27.126316,5.731579 C 26.889474,5.1078948 26.321052,4.231579 25.51579,3.6078948 25.042106,3.2526316 24.505264,2.9052632 23.747368,2.6842106 22.926316,2.4473684 21.91579,2.368421 20.68421,2.368421 H 9.3157894 c -1.2394736,0 -2.2421052,0.078948 -3.055263,0.3236842 C 5.5026316,2.9210526 4.9421052,3.2605264 4.4842106,3.6157894 3.6789474,4.231579 3.1105264,5.1157894 2.8657894,5.7394736 2.3763158,6.9947368 2.368421,8.4157894 2.368421,9.3157894 V 20.68421 c 0,0.9 0.0079,2.321054 0.4973684,3.584212 0.244737,0.623684 0.813158,1.5 1.6184212,2.123684 0.4657894,0.355262 1.018421,0.70263 1.7763158,0.923684 0.8131578,0.236842 1.8157894,0.315788 3.055263,0.315788 H 20.68421 c 1.239474,0 2.242106,-0.07894 3.055264,-0.323684 0.757894,-0.228946 1.31842,-0.56842 1.776316,-0.923684 0.805262,-0.615788 1.373684,-1.5 1.61842,-2.123684 0.497368,-1.255262 0.497368,-2.68421 0.497368,-3.58421 V 9.3078948 c 0,-0.9 -0.0078,-2.3210526 -0.497368,-3.5842106 z M 7.3736842,19.736842 H 6.4342106 V 14.123684 H 7.3736842 Z M 6.9078948,13.176316 c -0.3157896,0 -0.5842106,-0.260527 -0.5842106,-0.584211 0,-0.323684 0.2605264,-0.58421 0.5842106,-0.58421 0.3236842,0 0.5921052,0.260526 0.5921052,0.58421 -0.0079,0.323684 -0.268421,0.584211 -0.5921052,0.584211 z m 5.6447372,6.702631 c -2.478948,0 -4.0421056,-1.768421 -4.0421056,-4.586842 0,-2.818421 1.5631576,-4.594737 4.0421056,-4.594737 2.478947,0 4.042105,1.776316 4.042105,4.594737 0,2.818421 -1.563158,4.586842 -4.042105,4.586842 z m 8.178946,0 c -1.863157,0 -3.173683,-1.026315 -3.260525,-2.557894 h 0.844736 c 0.09474,1.065789 1.097369,1.792105 2.494737,1.792105 1.33421,0 2.289474,-0.726316 2.289474,-1.728947 0,-0.828948 -0.576316,-1.326316 -1.894736,-1.657895 L 20.1,15.442105 c -1.65,-0.418421 -2.384211,-1.113158 -2.384211,-2.289473 0,-1.436843 1.294737,-2.463158 3.047369,-2.463158 1.736842,0 3,1.026315 3.055264,2.439473 h -0.844738 c -0.07894,-1.002631 -0.971052,-1.673684 -2.23421,-1.673684 -1.231579,0 -2.147369,0.686842 -2.147369,1.66579Line truncated
/>
</svg>
);
export const IOSIcon = (props: Partial<CustomIconComponentProps>) => (
<Icon component={SvgIcon} {...props} />
);
@@ -0,0 +1,62 @@
import React, {useState} from 'react';
import {Button, Image, Typography} from "antd";
import TabSteps from "./TabSteps";
import { StepCommand } from "./types"
import applestore from '../../../../assets/app-store-badge.png';
import {getConfig} from "../../../../config";
const { grpcApiOrigin } = getConfig();
const {Text} = Typography;
export const IOSTab = () => {
const [steps, setSteps] = useState([
{
key: 1,
title: 'Download and install the application from App Store:',
commands: (
<a data-testid="download-android-button" href="https://apps.apple.com/app/netbird-p2p-vpn/id6469329339" target="_blank">
<Image width="12em" preview={false} style={{marginTop: "5px"}} src={applestore}/>
</a>
),
copied: false
} as StepCommand,
... grpcApiOrigin ? [{
key: 2,
title: 'Click on "Change Server" and enter the following "Server"',
commands: grpcApiOrigin,
commandsForCopy: grpcApiOrigin,
copied: false,
showCopyButton: false
}] : [],
{
key: 2 + (grpcApiOrigin ? 1 : 0),
title: 'Click on the "Connect" button in the middle of the screen',
commands: '',
copied: false,
showCopyButton: false
},
{
key: 3 + (grpcApiOrigin ? 1 : 0),
title: 'Sign up using your email address',
commands: '',
copied: false,
showCopyButton: false
}
])
return (
<div style={{marginTop: 10}}>
<Text style={{fontWeight: "bold"}}>
Install on Android
</Text>
<div style={{marginTop: 5}}>
<TabSteps stepsItems={steps}/>
</div>
</div>
)
}
export default IOSTab
+2
View File
@@ -8,6 +8,7 @@ export interface Account {
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
jwt_allow_groups: string[];
extra: {
peer_approval_enabled: boolean;
}
@@ -19,6 +20,7 @@ export interface FormAccount extends Account {
jwt_groups_enabled: boolean;
groups_propagation_enabled: boolean;
jwt_groups_claim_name: string;
jwt_allow_groups: string[];
peer_login_expiration_formatted: ExpiresInValue;
peer_approval_enabled: boolean;
}
+9 -1
View File
@@ -679,7 +679,15 @@ export const Peers = () => {
</Row>
</span>
</Button>
<Row>{loginExpire}</Row>
<Row style={{ minWidth: "195px", paddingLeft: "15px" }}>{loginExpire} <Button
type="text"
style={{ height: "auto", whiteSpace: "normal", textAlign: "center", padding: "0px", margin: "0px" }}
onClick={() => showConfirmApprove(peer)}
className={!isAdmin ? "nohover" : ""}
>
{approval}
</Button>
</Row>
</>
);
}
+205 -8
View File
@@ -50,6 +50,7 @@ import { actions as routeActions } from "../store/route";
import { actions as userActions } from "../store/user";
import {useOidc} from "@axa-fr/react-oidc";
import {getConfig} from "../config";
import {Link} from "react-router-dom";
const { Title, Paragraph, Text } = Typography;
@@ -76,6 +77,7 @@ export const Settings = () => {
];
const [groupsClicked, setGroupsClicked] = useState(false);
const [idpCLicked, setIdpClicked] = useState(false);
const [billingClicked, setBillingClicked] = useState(false);
const [authClicked, setAuthClicked] = useState(true);
const [dangerClicked, setDangerClicked] = useState(false);
@@ -140,6 +142,8 @@ export const Settings = () => {
const [groupsPropagationEnabled, setGroupsPropagationEnabled] =
useState(true);
const [jwtGroupsClaimName, setJwtGroupsClaimName] = useState("");
const [jwtAllowGroups, setJwtAllowGroups] = useState<string[]>([]);
const [displayJWTAllowGroups, setDisplayJWTAllowGroups] = useState(false);
const [confirmModal, confirmModalContextHolder] = Modal.useModal();
const { confirm } = Modal;
@@ -254,6 +258,7 @@ export const Settings = () => {
account.settings.peer_login_expiration_enabled,
jwt_groups_enabled: account.settings.jwt_groups_enabled,
jwt_groups_claim_name: account.settings.jwt_groups_claim_name,
jwt_allow_groups: account.settings.jwt_allow_groups? account.settings.jwt_allow_groups : [],
groups_propagation_enabled: account.settings.groups_propagation_enabled,
peer_approval_enabled: account.settings.extra ? account.settings.extra.peer_approval_enabled : false,
} as FormAccount;
@@ -263,6 +268,7 @@ export const Settings = () => {
setJwtGroupsEnabled(fAccount.jwt_groups_enabled);
setGroupsPropagationEnabled(fAccount.groups_propagation_enabled);
setJwtGroupsClaimName(fAccount.jwt_groups_claim_name);
setJwtAllowGroups(fAccount.jwt_allow_groups);
form.setFieldsValue(fAccount);
}, [accounts]);
@@ -428,9 +434,10 @@ export const Settings = () => {
jwt_groups_enabled: updatedAccount.data.settings.jwt_groups_enabled,
jwt_groups_claim_name:
updatedAccount.data.settings.jwt_groups_claim_name,
jwt_allow_groups: updatedAccount.data.settings.jwt_allow_groups,
groups_propagation_enabled:
updatedAccount.data.settings.groups_propagation_enabled,
peer_approval_enabled: updatedAccount.data.settings.extra.peer_approval_enabled
peer_approval_enabled: updatedAccount.data.settings.extra? updatedAccount.data.settings.extra.peer_approval_enabled : false,
} as FormAccount;
setFormAccount(fAccount);
} else if (updatedAccount.error) {
@@ -464,6 +471,7 @@ export const Settings = () => {
peer_login_expiration_enabled: formPeerExpirationEnabled,
jwt_groups_enabled: jwtGroupsEnabled,
jwt_groups_claim_name: jwtGroupsClaimName,
jwt_allow_groups: jwtAllowGroups,
groups_propagation_enabled: groupsPropagationEnabled,
peer_approval_enabled: formPeerApprovalEnabled,
});
@@ -493,6 +501,7 @@ export const Settings = () => {
peer_login_expiration_enabled: values.peer_login_expiration_enabled,
jwt_groups_enabled: jwtGroupsEnabled,
jwt_groups_claim_name: jwtGroupsClaimName,
jwt_allow_groups: jwtAllowGroups,
groups_propagation_enabled: groupsPropagationEnabled,
},
} as Account;
@@ -530,6 +539,7 @@ export const Settings = () => {
const saveAccount = (newValues: FormAccount) => {
let accountToSave = createAccountToSave(newValues);
dispatch(
accountActions.updateAccount.request({
getAccessTokenSilently: getTokenSilently,
@@ -657,24 +667,35 @@ export const Settings = () => {
setGroupsClicked(false);
setBillingClicked(false);
setDangerClicked(false);
setIdpClicked(false);
break;
case "groups":
setGroupsClicked(true);
setBillingClicked(false);
setAuthClicked(false);
setDangerClicked(false);
setIdpClicked(false);
break;
case "billing":
setBillingClicked(true);
setAuthClicked(false);
setGroupsClicked(false);
setDangerClicked(false);
setIdpClicked(false);
break;
case "danger":
setBillingClicked(false);
setAuthClicked(false);
setGroupsClicked(false);
setDangerClicked(true);
setIdpClicked(false);
break;
case "idp":
setIdpClicked(true);
setBillingClicked(false);
setAuthClicked(false);
setGroupsClicked(false);
setDangerClicked(false);
break;
}
};
@@ -702,12 +723,12 @@ export const Settings = () => {
"System settings",
"sub2",
<SettingOutlined />,
[getItem("Authentication", "auth"), getItem("Groups", "groups"), getItem("Danger zone", "danger", undefined, undefined, undefined, !isOwner)],
[getItem("Authentication", "auth"), getItem("Groups", "groups"), getItem("Identity provider", "idp", undefined, undefined, undefined, !(isNetBirdHosted() || isLocalDev())), getItem("Danger zone", "danger", undefined, undefined, undefined, !isOwner)],
"group"
),
];
useEffect(() => {}, [groupsClicked, billingClicked, authClicked, dangerClicked]);
useEffect(() => {}, [groupsClicked, billingClicked, authClicked, dangerClicked, idpCLicked]);
const renderGroupsSettingForm = () => {
return(
<>
@@ -862,6 +883,56 @@ export const Settings = () => {
</Form.Item>
</Col>
</Row>
<Row>
<Col span={12}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
JWT allow group
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Limit access to NetBird for the specified group name, e.g., NetBird users.
To use the group, first you need to configure it first in your IdP.
</Paragraph>
</Col>
</Row>
<Row>
<Col lg={6}>
<Form.Item name="jwt_allow_groups" style={displayJWTAllowGroups ? {marginBottom: "2px",} : {}}>
<Input
value={jwtAllowGroups[0]}
autoComplete="off"
onChange={(e) => {
setJwtAllowGroups([e.target.value]);
setDisplayJWTAllowGroups(true);
}}
/>
</Form.Item>
</Col>
</Row>
{ displayJWTAllowGroups && (
<Row>
<Paragraph style={{
marginTop: "-20",
fontWeight: "400",
marginBottom: "5px",
color: "red"
}}>
To prevent losing access, ensure you are part of this group.
</Paragraph>
</Row>
)}
</>
)}
</div>
@@ -909,9 +980,7 @@ export const Settings = () => {
>
Peer approval{" "}
<Tooltip
title="Peer approval requires that every newly added peer
will require approval by an administrator before it can connect to other peers.
You can approve peers in the peers tab."
title="When enabled, every newly added peer will require approval by an administrator before it can connect to other peers. You can approve peers in the peers tab."
>
<Text
style={{
@@ -1036,9 +1105,10 @@ export const Settings = () => {
/>
</Form.Item>
</div>
<Col
span={24}
style={{ marginTop: "10px", marginBottom: "24px" }}
style={{ marginTop: "-10px", marginBottom: "24px" }}
>
<Text type={"secondary"}>
Learn more about
@@ -1052,6 +1122,125 @@ export const Settings = () => {
</a>
</Text>
</Col>
{(isNetBirdHosted() || isLocalDev()) && (
<Col span={24}
style={{ marginTop: "10px", marginBottom: "24px" }}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Multi-factor authentication (MFA)
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
<Text type={"secondary"}>
If your IdP supports MFA, it will work automatically with NetBird. Otherwise, contact us at
<a
target="_blank"
rel="noreferrer"
href="mailto:support@netbird.io"
>
{" "}
support@netbird.io
{" "}
</a>
to enable this feature
</Text>
</Paragraph>
</Col>)}
</>
)
}
const renderIdpSettingsForm = () => {
return (
<>
<div
style={{
color: "rgba(0, 0, 0, 0.88)",
fontWeight: "500",
fontSize: "18px",
marginBottom: "20px",
}}
>
Identity provider
</div>
<div >
<Row>
<Col span={24}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
User and group sync
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Sync users and groups from your IdP to use them in access control
</Paragraph>
</Col>
</Row>
<Col span={24}
style={{ marginTop: "10px", marginBottom: "24px" }}>
<label
style={{
color: "rgba(0, 0, 0, 0.88)",
fontSize: "14px",
fontWeight: "500",
}}
>
Custom IdP
</label>
<Paragraph
type={"secondary"}
style={{
marginTop: "-2",
fontWeight: "400",
marginBottom: "5px",
}}
>
Configure your custom IdP like Okta or Jumpcloud
</Paragraph>
</Col>
<Col
span={24}
style={{ marginTop: "10px", marginBottom: "24px" }}
>
<Text type={"secondary"}>
To enable these features, please contact us at
<a
target="_blank"
rel="noreferrer"
href="mailto:support@netbird.io"
>
{" "}
support@netbird.io
</a>
</Text>
</Col>
</div>
</>
)
}
@@ -1127,6 +1316,9 @@ export const Settings = () => {
if (dangerClicked) {
loaded = renderDangerSettingsForm()
}
if (idpCLicked) {
loaded = renderIdpSettingsForm()
}
return (
<Form
@@ -1137,7 +1329,7 @@ export const Settings = () => {
>
<Card loading={loading} defaultValue={"Enabled"}>
{loaded}
{!dangerClicked && (<Form.Item style={{ marginBottom: "0" }}>
{!(dangerClicked || idpCLicked) && (<Form.Item style={{ marginBottom: "0" }}>
<Button type="primary" htmlType="submit">
Save
</Button>
@@ -1568,6 +1760,11 @@ export const Settings = () => {
<Col span={24}>{renderSettingForm()}</Col>
</Row>
)}
{idpCLicked && (
<Row style={{ marginTop: "0", width: "100%" }}>
<Col span={24}>{renderSettingForm()}</Col>
</Row>
)}
</Col>
</Row>
</Container>