diff --git a/src/store/account/actions.ts b/src/store/account/actions.ts index 78c9a2e4..4bccda09 100644 --- a/src/store/account/actions.ts +++ b/src/store/account/actions.ts @@ -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 = { ), ChangeResponse, ChangeResponse>(), setUpdateAccount: createAction('SET_UPDATED_ACCOUNT')>(), resetUpdateAccount: createAction('RESET_UPDATED_ACCOUNT')(), + deleteAccount: createAsyncAction( + 'DELETE_ACCOUNT_REQUEST', + 'DELETE_ACCOUNT_SUCCESS', + 'DELETE_ACCOUNT_FAILURE' + ), DeleteResponse, DeleteResponse>(), + resetDeletedAccount: createAction('RESET_DELETED_ACCOUNT')(), + setDeleteAccount: createAction('SET_DELETE_ACCOUNT')>(), }; diff --git a/src/store/account/reducer.ts b/src/store/account/reducer.ts index 180af7c6..54de23de 100644 --- a/src/store/account/reducer.ts +++ b/src/store/account/reducer.ts @@ -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; + deletedAccount: DeleteResponse; }>; const initialState: StateType = { @@ -22,6 +23,13 @@ const initialState: StateType = { error: null, data : null }, + deletedAccount: >{ + loading: false, + success: false, + failure: false, + error: null, + data : null + }, }; const data = createReducer(initialState.data as Account[]) @@ -45,10 +53,18 @@ const updatedAccount = createReducer, ActionTypes .handleAction(actions.setUpdateAccount, (store, action) => action.payload) .handleAction(actions.resetUpdateAccount, () => initialState.savedAccount) +const deleteAccount = createReducer, 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 }); diff --git a/src/store/account/sagas.ts b/src/store/account/sagas.ts index 42fea20d..e00a03f6 100644 --- a/src/store/account/sagas.ts +++ b/src/store/account/sagas.ts @@ -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): Generator { try { @@ -55,10 +56,55 @@ export function* updateAccount(action: ReturnType +): Generator { + try { + yield call(actions.setDeleteAccount, { + loading: true, + success: false, + failure: false, + error: null, + data: null, + } as DeleteResponse); + + const effect = yield call(service.deleteAccount, action.payload); + const response = effect as ApiResponse; + + yield put( + actions.deleteAccount.success({ + loading: false, + success: true, + failure: false, + error: null, + data: response.body, + } as DeleteResponse) + ); + + 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) + ); + } +} + export default function* sagas(): Generator { yield all([ takeLatest(actions.getAccounts.request, getAccounts), takeLatest(actions.updateAccount.request, updateAccount), + takeLatest(actions.deleteAccount.request, deleteAccount), ]); } diff --git a/src/store/account/service.ts b/src/store/account/service.ts index 128597ef..f05d5792 100644 --- a/src/store/account/service.ts +++ b/src/store/account/service.ts @@ -15,5 +15,11 @@ export default { `/api/accounts/${id}`, payload ); - } + }, + async deleteAccount(payload:RequestPayload): Promise> { + return apiClient.delete( + `/api/accounts/` + payload.payload, + payload + ); + }, }; diff --git a/src/views/Settings.tsx b/src/views/Settings.tsx index efa9e98f..1f0317c8 100644 --- a/src/views/Settings.tsx +++ b/src/views/Settings.tsx @@ -18,8 +18,6 @@ import { Radio, Input, RadioChangeEvent, - Alert, - Progress, Menu, MenuProps, } from "antd"; @@ -50,12 +48,15 @@ import { actions as policyActions } from "../store/policy"; import { actions as nsGroupActions } from "../store/nameservers"; 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"; const { Title, Paragraph, Text } = Typography; const styleNotification = { marginTop: 85 }; export const Settings = () => { + const { logout } = useOidc(); const { getTokenSilently } = useGetTokenSilently(); const dispatch = useDispatch(); const { pageSize, onChangePageSize, pageSizeOptions } = usePageSizeHelpers( @@ -77,6 +78,11 @@ export const Settings = () => { const [groupsClicked, setGroupsClicked] = useState(false); const [billingClicked, setBillingClicked] = useState(false); const [authClicked, setAuthClicked] = useState(true); + const [dangerClicked, setDangerClicked] = useState(false); + const [accountDeleting, setAccountDeleting] = useState(false); + + const [isOwner, setIsOwner] = useState(false); + const [filterGroup, setFilterGroup] = useState([]); const [textToSearch, setTextToSearch] = useState( @@ -88,6 +94,7 @@ export const Settings = () => { const {} = useGetGroupTagHelpers(); const accounts = useSelector((state: RootState) => state.account.data); + const accountDeleted = useSelector((state: RootState) => state.account.deleteAccount); const failed = useSelector((state: RootState) => state.account.failed); const loading = useSelector((state: RootState) => state.account.loading); const updatedAccount = useSelector( @@ -137,6 +144,16 @@ export const Settings = () => { const { confirm } = Modal; const [form] = Form.useForm(); + + useEffect(() => { + if (users) { + let currentUser = users.find((user) => user?.is_current); + if (currentUser) { + setIsOwner(currentUser.role === "owner"); + } + } + }, [users]); + useEffect(() => { dispatch( accountActions.getAccounts.request({ @@ -202,6 +219,21 @@ export const Settings = () => { }; useEffect(() => { + if (accountDeleted.success) { + showDeleteAccountMSG() + return + } + + if (accountDeleted.failure) { + setAccountDeleting(false) + return + } + }, [accountDeleted]); + + useEffect(() => { + if (accounts.length < 1 && accountDeleting) { + return; + } if (accounts.length < 1) { console.debug( "invalid account data returned from the Management API", @@ -514,7 +546,7 @@ export const Settings = () => { return false; }; - const showConfirmDelete = (record: any) => { + const showConfirmDeleteGroup = (record: any) => { confirm({ icon: , title: Delete group {record.name}, @@ -537,6 +569,54 @@ export const Settings = () => { onCancel() {}, }); }; + + const showConfirmDeleteAccount = () => { + confirm({ + icon: , + title: Delete NetBird Account, + okText: "Delete", + width: 600, + content: ( + + Are you sure you want to delete your NetBird account? + + ), + okType: "danger", + onOk() { + setAccountDeleting(true) + dispatch( + accountActions.deleteAccount.request({ + getAccessTokenSilently: getTokenSilently, + payload: accounts[0].id, + }) + ); + }, + onCancel() {}, + }); + }; + const config = getConfig(); + const showDeleteAccountMSG = () => { + setTimeout( + () => {logout("",{client_id: config.clientId})}, 5000); + confirm({ + icon: , + title: NetBird Account deleted, + okText: "Logout now", + width: 600, + content: ( + + Your account has been deleted. Your session will log out from your session in 5 seconds. + + ), + okType: "primary", + onOk() { + logout("",{client_id: config.clientId}) + }, + onCancel() {}, + }); + }; + + const deleteKey = "deleting"; useEffect(() => { const style = { marginTop: 85 }; @@ -570,16 +650,25 @@ export const Settings = () => { setAuthClicked(true); setGroupsClicked(false); setBillingClicked(false); + setDangerClicked(false); break; case "groups": setGroupsClicked(true); setBillingClicked(false); setAuthClicked(false); + setDangerClicked(false); break; case "billing": setBillingClicked(true); setAuthClicked(false); setGroupsClicked(false); + setDangerClicked(false); + break; + case "danger": + setBillingClicked(false); + setAuthClicked(false); + setGroupsClicked(false); + setDangerClicked(true); break; } }; @@ -589,7 +678,8 @@ export const Settings = () => { key: React.Key, icon?: React.ReactNode, children?: MenuItem[], - type?: "group" + type?: "group", + disabled?: boolean ): MenuItem { return { key, @@ -597,6 +687,7 @@ export const Settings = () => { children, label, type, + disabled, } as MenuItem; } @@ -605,81 +696,236 @@ export const Settings = () => { "System settings", "sub2", , - [getItem("Authentication", "auth"), getItem("Groups", "groups")], + [getItem("Authentication", "auth"), getItem("Groups", "groups"), getItem("Danger zone", "danger", undefined, undefined, undefined, !isOwner)], "group" ), ]; - useEffect(() => {}, [groupsClicked, billingClicked, authClicked]); - const renderSettingForm = () => { + useEffect(() => {}, [groupsClicked, billingClicked, authClicked, dangerClicked]); + const renderGroupsSettingForm = () => { + return( + <> +
+ User groups +
+
+ + + +
+ { + setGroupsPropagationEnabled(checked); + }} + size="small" + checked={groupsPropagationEnabled} + /> +
+ + + Allow group propagation from user’s auto-groups to + peers, sharing membership information + +
+
+
+ +
+ {(!isNetBirdHosted() || isLocalDev()) && ( + <> + + + +
+ { + setJwtGroupsEnabled(checked); + }} + size="small" + checked={jwtGroupsEnabled} + /> +
+ + + Extract & sync groups from JWT claims with user’s + auto-groups, auto-creating groups from tokens. + +
+
+
+ +
+ + + + + Specify the JWT claim for extracting group names, e.g., + roles or groups, to add to account groups (this claim should contain a list of group names). + + + + + + + { + if (event.code === "Space") event.preventDefault(); + }} + onChange={(e) => { + let val = e.target.value; + var t = val.replace(/ /g, ""); + setJwtGroupsClaimName(t); + }} + /> + + + + + )} +
+ + ) + } + + const renderAuthSettingsForm = () => { return ( -
- + <>
- {groupsClicked ? "User groups" : "Authentication"} + Authentication
-
+
{(isNetBirdHosted() || isLocalDev()) &&
{ - setFormPeerApprovalEnabled(checked); - }} - size="small" - checked={formPeerApprovalEnabled} + onChange={(checked) => { + setFormPeerApprovalEnabled(checked); + }} + size="small" + checked={formPeerApprovalEnabled} />
Require peers to be approved by an administrator @@ -688,52 +934,52 @@ export const Settings = () => { }
{ - setFormPeerExpirationEnabled(checked); - }} - size="small" - checked={formPeerExpirationEnabled} + onChange={(checked) => { + setFormPeerExpirationEnabled(checked); + }} + size="small" + checked={formPeerExpirationEnabled} />
Request periodic re-authentication of peers registered with SSO @@ -746,21 +992,21 @@ export const Settings = () => { Time after which every peer added with SSO login will require re-authentication @@ -769,187 +1015,127 @@ export const Settings = () => {
-
+ + + Learn more about + + {" "} + login expiration + + + + + ) + } + + const renderDangerSettingsForm = () => { + return ( + <> +
+ Danger zone +
+
- +
- { - setGroupsPropagationEnabled(checked); + style={{ + display: "flex", + gap: "15px", }} - size="small" - checked={groupsPropagationEnabled} - /> + >
- Allow group propagation from user’s auto-groups to - peers, sharing membership information + Before proceeding to delete your Netbird account, please be aware that this action is irreversible. + Once your account is deleted, you will permanently lose access to all associated data, + including your peers, users, groups, policies, and routes.
+ + +
- {(!isNetBirdHosted() || isLocalDev()) && ( - <> - - - -
- { - setJwtGroupsEnabled(checked); - }} - size="small" - checked={jwtGroupsEnabled} - /> -
- - - Extract & sync groups from JWT claims with user’s - auto-groups, auto-creating groups from tokens. - -
-
-
- -
- - - - - Specify the JWT claim for extracting group names, e.g., - roles or groups, to add to account groups (this claim should contain a list of group names). - - - - - - - { - if (event.code === "Space") event.preventDefault(); - }} - onChange={(e) => { - let val = e.target.value; - var t = val.replace(/ /g, ""); - setJwtGroupsClaimName(t); - }} - /> - - - - - )}
- - - Learn more about - - {" "} - login expiration - - - - + + ) + } + + const renderSettingForm = () => { + let loaded = renderAuthSettingsForm() + if(groupsClicked) { + loaded = renderGroupsSettingForm() + } + if (dangerClicked) { + loaded = renderDangerSettingsForm() + } + + return ( + + + {loaded} + {!dangerClicked && ( - + )} ); @@ -1355,7 +1541,7 @@ export const Settings = () => { type={"text"} disabled={isButtonDisabled} onClick={() => { - showConfirmDelete(record); + showConfirmDeleteGroup(record); }} > Delete @@ -1371,6 +1557,11 @@ export const Settings = () => { )} + {dangerClicked && ( + + {renderSettingForm()} + + )}