prparing for service config flow

This commit is contained in:
balzack 2025-02-13 22:10:58 -08:00
parent 0f5232c371
commit 1023d0781e
17 changed files with 369 additions and 34 deletions

View File

@ -6,7 +6,7 @@ import {NativeRouter} from 'react-router-native';
import {Routes, Route} from 'react-router-dom';
import {Root} from './src/root/Root';
import {Access} from './src/access/Access';
import {Node} from './src/node/Node';
import {Service} from './src/service/Service';
import {Session} from './src/session/Session';
import {useColorScheme} from 'react-native';
@ -120,7 +120,7 @@ function App(): React.JSX.Element {
<Routes>
<Route path="/" element={<Text>EMPTY</Text>} />
<Route path="/access" element={<Access />} />
<Route path="/node" element={<Node />} />
<Route path="/service" element={<Service />} />
<Route path="/session" element={<Session />} />
</Routes>
</NativeRouter>

View File

@ -257,7 +257,7 @@ export function Access() {
)}
{state.mode === 'admin' && (
<View style={styles.body}>
<Text variant="headlineSmall">{state.strings.adminAccess}</Text>
<Text variant="headlineSmall">{state.strings.admin}</Text>
<TextInput
style={styles.input}
mode="flat"

View File

@ -0,0 +1,8 @@
import React from 'react';
import {SafeAreaView, Image, View, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
export function Accounts() {
return <Text>ACCOUNTS</Text>
}

View File

@ -1,5 +1,5 @@
import {useState, useEffect, useRef} from 'react';
import {DatabagSDK, Session, Focus} from 'databag-client-sdk';
import {DatabagSDK, Service, Session, Focus} from 'databag-client-sdk';
import {Platform, PermissionsAndroid} from 'react-native';
import {SessionStore} from '../SessionStore';
import {NativeCrypto} from '../NativeCrypto';
@ -44,6 +44,7 @@ export function useAppContext() {
const local = useRef(new LocalStore());
const sdk = useRef(databag);
const [state, setState] = useState({
service: null as null | Service,
session: null as null | Session,
focus: null as null | Focus,
fullDayTime: false,
@ -179,11 +180,11 @@ export function useAppContext() {
return await sdk.current.username(username, token, node, secure);
},
adminLogin: async (token: string, node: string, secure: boolean, code: string) => {
const login = await sdk.current.configure(node, secure, token, code);
updateState({node: login});
const service = await sdk.current.configure(node, secure, token, code);
updateState({ service });
},
adminLogout: async () => {
updateState({node: null});
updateState({service: null});
},
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,21 +0,0 @@
import {Button} from 'react-native-paper';
import {Text} from 'react-native';
import {AppContext} from '../context/AppContext';
import {View} from 'react-native';
import React, {useContext} from 'react';
export function Node() {
const app = useContext(AppContext);
return (
<View>
<Text>NODE!</Text>
<Text>NODE!</Text>
<Text>NODE!</Text>
<Text>NODE!</Text>
<Button mode="contained" onPress={app.actions.adminLogout}>
LOGOUT
</Button>
</View>
);
}

View File

@ -34,16 +34,16 @@ export function useRoot() {
navigate('/');
} else if (state.pathname === '/session' && !app.state.session) {
navigate('/');
} else if (state.pathname === '/node' && !app.state.node) {
} else if (state.pathname === '/service' && !app.state.service) {
navigate('/');
} else if (state.pathname === '/' && !app.state.session && !app.state.node) {
} else if (state.pathname === '/' && !app.state.session && !app.state.service) {
navigate('/access');
} else if (state.pathname !== '/node' && app.state.node) {
navigate('/node');
} else if (state.pathname !== '/service' && app.state.service) {
navigate('/service');
} else if (state.pathname !== '/session' && app.state.session) {
navigate('/session');
}
}, [state.pathname, app.state.session, app.state.node, app.state.initialized, navigate]);
}, [state.pathname, app.state.session, app.state.service, app.state.initialized, navigate]);
const actions = {};

View File

@ -0,0 +1,111 @@
import {StyleSheet} from 'react-native';
import { Colors } from '../constants/Colors';
export const styles = StyleSheet.create({
service: {
position: 'relative',
},
container: {
width: '100%',
height: '100%',
},
noHeader: {
headerBackTitleVisible: false,
},
full: {
width: '100%',
height: '100%',
position: 'relative',
},
alert: {
position: 'absolute',
top: '33%',
width: '100%',
display: 'flex',
flexDirection: 'row',
justifyContent: 'center',
alignContent: 'center',
},
alertArea: {
display: 'flex',
flexDirection: 'row',
gap: 8,
paddingTop: 8,
paddingBottom: 8,
paddingLeft: 16,
paddingRight: 16,
borderRadius: 16,
},
alertLabel: {
color: Colors.offsync,
padding: 0,
textAlign: 'center',
lineHeight: 20,
fontSize: 16,
},
frame: {
display: 'flex',
flexDirection: 'row',
width: '100%',
height: '100%',
},
left: {
display: 'flex',
flexDirection: 'column',
height: '100%',
width: '33%',
maxWidth: 300,
},
right: {
height: '100%',
display: 'flex',
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
},
workarea: {
height: '100%',
display: 'flex',
flexGrow: 1,
flexShrink: 1,
minWidth: 0,
},
identity: {
flexShrink: 0,
paddingBottom: 4,
},
channels: {
flexGrow: 1,
height: 1,
},
screen: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
},
body: {
width: '100%',
flexGrow: 1,
flexShrink: 1,
},
tabs: {
flexShrink: 0,
height: 64,
width: '100%',
displlay: 'flex',
flexDirection: 'row',
},
idleTab: {
flex: 1,
backgroundColor: 'transparent',
opacity: 0.5,
},
activeTab: {
flex: 1,
backgroundColor: 'transparent',
},
ring: {
paddingLeft: 16,
},
});

View File

@ -0,0 +1,143 @@
import React, {useState, useCallback, useEffect} from 'react';
import {SafeAreaView, Pressable, View, useColorScheme} from 'react-native';
import {styles} from './Service.styled';
import {IconButton, Surface, Text, Icon} from 'react-native-paper';
import {Accounts} from '../accounts/Accounts';
import {Setup} from '../setup/Setup';
import {useService} from './useService.hook';
import {NavigationContainer, DefaultTheme, DarkTheme} from '@react-navigation/native';
import {createDrawerNavigator} from '@react-navigation/drawer';
import {Colors} from '../constants/Colors';
const SetupDrawer = createDrawerNavigator();
export function Service() {
const { state, actions } = useService();
const [tab, setTab] = useState('accounts');
const scheme = useColorScheme();
const showAccounts = {display: tab === 'accounts' ? 'flex' : 'none'};
const showSetup = {display: tab === 'setup' ? 'flex' : 'none'};
return (
<View style={styles.service}>
{state.layout !== 'large' && (
<Surface elevation={3}>
<SafeAreaView style={styles.full}>
<View style={styles.screen}>
<View
style={{
...styles.body,
...showAccounts,
}}>
<Accounts />
</View>
<View
style={{
...styles.body,
...showSetup,
}}>
<Setup />
</View>
<View style={styles.tabs}>
{tab === 'accounts' && (
<IconButton
style={styles.activeTab}
mode="contained"
icon={'contacts'}
size={28}
onPress={() => {
setTab('accounts');
}}
/>
)}
{tab !== 'accounts' && (
<IconButton
style={styles.idleTab}
mode="contained"
icon={'contacts-outline'}
size={28}
onPress={() => {
setTab('accounts');
}}
/>
)}
{tab === 'setup' && (
<IconButton
style={styles.activeTab}
mode="contained"
icon={'cog'}
size={28}
onPress={() => {
setTab('setup');
}}
/>
)}
{tab !== 'setup' && (
<IconButton
style={styles.idleTab}
mode="contained"
icon={'cog-outline'}
size={28}
onPress={() => {
setTab('setup');
}}
/>
)}
</View>
</View>
</SafeAreaView>
</Surface>
)}
{state.layout === 'large' && (
<NavigationContainer theme={scheme === 'dark' ? DarkTheme : DefaultTheme}>
<View style={styles.container}>
<SetupScreen />
</View>
</NavigationContainer>
)}
</View>
);
}
function SetupScreen() {
const SetupComponent = useCallback(
() => (
<Surface elevation={3} mode="flat">
<SafeAreaView>
<Setup />
</SafeAreaView>
</Surface>
),
[],
);
return (
<SetupDrawer.Navigator
id="SetupDrawer"
drawerContent={SetupComponent}
screenOptions={{
drawerStyle: {width: '50%'},
drawerPosition: 'right',
drawerType: 'front',
headerShown: false,
overlayColor: 'rgba(8,8,8,.9)',
}}>
<SetupDrawer.Screen name="home">{({navigation}) => <AccountScreen setup={navigation} />}</SetupDrawer.Screen>
</SetupDrawer.Navigator>
);
}
function AccountScreen({setup}) {
return (
<View style={styles.frame}>
<Accounts />
<IconButton
mode="contained"
icon={'cog'}
size={28}
onPress={setup.openDrawer}
/>
</View>
);
}

View File

@ -0,0 +1,31 @@
import {useEffect, useState, useContext, useRef} from 'react';
import {AppContext} from '../context/AppContext';
import {DisplayContext} from '../context/DisplayContext';
import {ContextType} from '../context/ContextType';
export function useService() {
const display = useContext(DisplayContext) as ContextType;
const app = useContext(AppContext) as ContextType;
const [state, setState] = useState({
layout: null,
strings: {},
});
const updateState = (value: any) => {
setState(s => ({...s, ...value}));
};
useEffect(() => {
const {layout, strings} = display.state;
updateState({layout, strings});
}, [display.state]);
const actions = {
logout: async () => {
await app.actions.adminLogout();
},
};
return {state, actions};
}

View File

@ -0,0 +1,8 @@
import React from 'react';
import {SafeAreaView, Image, View, Pressable} from 'react-native';
import {Text} from 'react-native-paper';
export function Setup() {
return <Text>SETUP</Text>
}

View File

@ -142,7 +142,7 @@ export class DatabagSDK {
public async configure(node: string, secure: boolean, token: string, mfaCode: string | null): Promise<Service> {
const access = await setAdmin(node, secure, token, mfaCode);
return new ServiceModule(this.log, node, secure, token);
return new ServiceModule(this.log, node, secure, access);
}
public async automate(node: string, secure: boolean, token: string): Promise<Contributor> {

View File

@ -0,0 +1,9 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function addAdminMFAuth(server: string, secure: boolean, token: string): Promise<{ secretImage: string, secretText: string }> {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/mfauth?token=${token}`;
const mfa = await fetchWithTimeout(endpoint, { method: 'POST' });
checkResponse(mfa.status);
return await mfa.json();
}

View File

@ -0,0 +1,9 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function getAdminMFAuth(server: string, secure: boolean, token: string): Promise<boolean> {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/mfauth?token=${token}`;
const mfa = await fetchWithTimeout(endpoint, { method: 'GET' });
checkResponse(mfa.status);
return await mfa.json();
}

View File

@ -0,0 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function removeAdminMFAuth(server: string, secure: boolean, token: string) {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/mfauth?token=${token}`;
const { status } = await fetchWithTimeout(endpoint, { method: 'DELETE' });
checkResponse(status);
}

View File

@ -0,0 +1,8 @@
import { checkResponse, fetchWithTimeout } from './fetchUtil';
export async function setAdminMFAuth(server: string, secure: boolean, token: string, code: string): Promise<void> {
const endpoint = `http${secure ? 's' : ''}://${server}/admin/mfauth?token=${token}&code=${code}`;
const { status } = await fetchWithTimeout(endpoint, { method: 'PUT' });
checkResponse(status);
}

View File

@ -1,6 +1,10 @@
import type { Service } from './api';
import type { Member, Setup } from './types';
import type { Logging } from './logging';
import { getAdminMFAuth } from './net/getAdminMFAuth';
import { setAdminMFAuth } from './net/setAdminMFAuth';
import { addAdminMFAuth } from './net/addAdminMFAuth';
import { removeAdminMFAuth } from './net/removeAdminMFAuth';
export class ServiceModule implements Service {
private log: Logging;
@ -54,4 +58,20 @@ export class ServiceModule implements Service {
}
public async setSetup(config: Setup): Promise<void> {}
public async enableMFA(): Promise<{ secretImage: string, secretText: string}> {
const { node, secure, token } = this;
const { secretImage, secretText } = await addAdminMFAuth(node, secure, token);
return { secretImage, secretText };
}
public async disableMFA(): Promise<void> {
const { node, secure, token } = this;
await removeAdminMFAuth(node, secure, token);
}
public async confirmMFA(code: string): Promise<void> {
const { node, secure, token } = this;
await setAdminMFAuth(node, secure, token, code);
}
}