UNPKG

erebrus-react-native-sdk

Version:

React Native SDK for Erebrus VPN

413 lines (407 loc) 17.7 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.ClientCreator = void 0; require("react-native-get-random-values"); const buffer_1 = require("buffer"); if (typeof global.Buffer === 'undefined') { global.Buffer = buffer_1.Buffer; } const react_1 = __importStar(require("react")); const react_native_1 = require("react-native"); const axios_1 = __importDefault(require("axios")); const curve25519_js_1 = require("curve25519-js"); const react_native_qrcode_svg_1 = __importDefault(require("react-native-qrcode-svg")); const Auth_1 = require("./Auth"); const REGIONS = [ { id: "SG", name: "Singapore" }, { id: "IN", name: "India" }, { id: "US", name: "United States" }, { id: "JP", name: "Japan" }, { id: "CA", name: "Canada" }, { id: "GB", name: "United Kingdom" }, { id: "AU", name: "Australia" }, { id: "DE", name: "Germany" }, ]; const ClientCreator = ({ apiConfig, onClientCreated, theme = { surface: '#ffffff', background: '#f5f5f5', text: '#000000', textSecondary: '#6b7280', primary: '#6366f1', border: '#e5e7eb', }, }) => { const [newClientName, setNewClientName] = (0, react_1.useState)(''); const [selectedRegion, setSelectedRegion] = (0, react_1.useState)(''); const [isCreatingClient, setIsCreatingClient] = (0, react_1.useState)(false); const [nodesData, setNodesData] = (0, react_1.useState)([]); const [selectedNodeId, setSelectedNodeId] = (0, react_1.useState)(''); const [selectedNodeName, setSelectedNodeName] = (0, react_1.useState)(''); const [showNodeModal, setShowNodeModal] = (0, react_1.useState)(false); const [showQrCode, setShowQrCode] = (0, react_1.useState)(false); const [configFile, setConfigFile] = (0, react_1.useState)(''); const [token, setToken] = (0, react_1.useState)(apiConfig.token); const [isLoadingNodes, setIsLoadingNodes] = (0, react_1.useState)(false); const [nodesError, setNodesError] = (0, react_1.useState)(null); const handleTokenReceived = (newToken) => { setToken(newToken); }; const generateKeys = () => { try { const randomBytesArray = new Uint8Array(32); crypto.getRandomValues(randomBytesArray); const keyPair = (0, curve25519_js_1.generateKeyPair)(randomBytesArray); const privKey = buffer_1.Buffer.from(keyPair.private).toString('base64'); const pubKey = buffer_1.Buffer.from(keyPair.public).toString('base64'); const preSharedKeyArray = new Uint8Array(32); crypto.getRandomValues(preSharedKeyArray); const preSharedKeyB64 = buffer_1.Buffer.from(preSharedKeyArray).toString('base64'); return { preSharedKey: preSharedKeyB64, privKey, pubKey, }; } catch (error) { console.error('Key generation failed:', error); throw new Error('Failed to generate cryptographic keys'); } }; const fetchNodesData = (0, react_1.useCallback)(async () => { if (!token) { setNodesError('Please authenticate first'); return; } setIsLoadingNodes(true); setNodesError(null); try { const response = await axios_1.default.get(`${apiConfig.gatewayUrl}api/v1.0/nodes/all`, { headers: { Accept: 'application/json', 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, }); if (response.status === 200) { const payload = response.data.payload; const filteredNodes = payload.filter((node) => node.status === 'active' && node.region !== undefined && node.region !== null && node.region.trim()); setNodesData(filteredNodes); } else { setNodesError('Failed to fetch available nodes'); } } catch (error) { setNodesError('Failed to fetch available nodes'); console.error('Error fetching nodes data:', error); } finally { setIsLoadingNodes(false); } }, [apiConfig, token]); react_1.default.useEffect(() => { fetchNodesData(); }, [fetchNodesData]); const createVPNClient = (0, react_1.useCallback)(async () => { if (!token) { react_native_1.Alert.alert('Error', 'Please authenticate first'); return; } const selectedNode = nodesData.find((n) => n.id === selectedNodeId); if (!selectedNode) { react_native_1.Alert.alert('Error', 'Please select a node first'); return; } setIsCreatingClient(true); try { const keys = generateKeys(); const requestData = { name: newClientName, presharedKey: keys.preSharedKey, publicKey: keys.pubKey, }; const url = `${apiConfig.gatewayUrl}api/v1.0/erebrus/client/${selectedNodeId}`; console.log('Token used for client creation:', token); console.log('POST URL:', url); console.log('Request body:', requestData); const response = await fetch(url, { method: 'POST', headers: { Accept: 'application/json, text/plain, */*', 'Content-Type': 'application/json', Authorization: `Bearer ${token}`, }, body: JSON.stringify(requestData), }); if (response.status === 200) { const data = await response.json(); const client = data.payload.client; const configFile = ` [Interface] Address = ${client.Address[0]} PrivateKey = ${keys.privKey} DNS = 1.1.1.1 [Peer] PublicKey = ${data.payload.serverPublicKey} PresharedKey = ${client.PresharedKey} AllowedIPs = 0.0.0.0/0, ::/0 Endpoint = ${data.payload.endpoint}:51820 PersistentKeepalive = 16`; setConfigFile(configFile); setShowQrCode(true); const vpnConfig = { privateKey: keys.privKey, publicKey: keys.pubKey, serverAddress: data.payload.endpoint, serverPort: 51820, allowedIPs: ['0.0.0.0/0', '::/0'], dns: ['1.1.1.1', '8.8.8.8'], mtu: 1280, presharedKey: client.PresharedKey, persistentKeepalive: 16, }; onClientCreated({ configFile, vpnConfig }); } else { const errorText = await response.text(); console.error('Failed to create VPN client:', errorText); react_native_1.Alert.alert('Error', `Failed to create VPN client: ${errorText}`); } } catch (error) { console.error('Failed to create VPN client:', error); let errorMessage = 'Failed to create VPN client: '; errorMessage += error.message || 'Unknown error occurred'; react_native_1.Alert.alert('Error', errorMessage); } finally { setIsCreatingClient(false); } }, [apiConfig, token, selectedNodeId, newClientName, nodesData, onClientCreated]); return (<react_native_1.ScrollView style={[styles.container, { backgroundColor: theme.background }]}> {!token ? (<Auth_1.Auth onTokenReceived={handleTokenReceived}/>) : (<> <react_native_1.View style={[styles.section, { backgroundColor: theme.surface }]}> <react_native_1.Text style={[styles.sectionTitle, { color: theme.text }]}>Create New Client</react_native_1.Text> <react_native_1.View style={styles.form}> <react_native_1.Text style={[styles.label, { color: theme.text }]}>Client Name</react_native_1.Text> <react_native_1.TextInput style={[ styles.input, { backgroundColor: theme.background, borderColor: theme.border, color: theme.text, }, ]} value={newClientName} onChangeText={setNewClientName} placeholder="Enter client name (max 8 chars)" placeholderTextColor={theme.textSecondary} maxLength={8}/> <react_native_1.Text style={[styles.label, { color: theme.text }]}>Region</react_native_1.Text> <react_native_1.ScrollView horizontal style={{ marginBottom: 16 }} showsHorizontalScrollIndicator={false}> {REGIONS.map((region) => (<react_native_1.TouchableOpacity key={region.id} style={[ styles.regionButton, { backgroundColor: selectedRegion === region.id ? theme.primary : theme.background, borderColor: theme.border, }, ]} onPress={() => { setSelectedRegion(region.id); setSelectedNodeId(''); setSelectedNodeName(''); }}> <react_native_1.Text style={{ color: selectedRegion === region.id ? '#fff' : theme.text }}> {region.name} </react_native_1.Text> </react_native_1.TouchableOpacity>))} </react_native_1.ScrollView> <react_native_1.Text style={[styles.label, { color: theme.text }]}>Node</react_native_1.Text> {isLoadingNodes ? (<react_native_1.ActivityIndicator size="small" color={theme.primary} style={{ marginVertical: 10 }}/>) : nodesError ? (<react_native_1.Text style={{ color: '#ef4444', marginBottom: 10 }}>{nodesError}</react_native_1.Text>) : (<> <react_native_1.TouchableOpacity style={[ styles.nodeSelectButton, { backgroundColor: theme.background, borderColor: theme.border, }, ]} onPress={() => setShowNodeModal(true)} disabled={!selectedRegion || nodesData.filter((n) => n.region === selectedRegion).length === 0}> <react_native_1.Text style={{ color: theme.text }}> {selectedNodeName || 'Select Node'} </react_native_1.Text> </react_native_1.TouchableOpacity> <react_native_1.Modal visible={showNodeModal} animationType="slide" transparent={true} onRequestClose={() => setShowNodeModal(false)}> <react_native_1.View style={styles.modalOverlay}> <react_native_1.View style={[styles.modalContent, { backgroundColor: theme.surface }]}> <react_native_1.Text style={[styles.sectionTitle, { color: theme.text, marginBottom: 10 }]}>Select Node</react_native_1.Text> <react_native_1.FlatList data={nodesData.filter((node) => node.region === selectedRegion)} keyExtractor={(item) => item.id} renderItem={({ item }) => (<react_native_1.TouchableOpacity style={[ styles.nodeItem, { backgroundColor: selectedNodeId === item.id ? theme.primary : theme.background, borderColor: theme.border, }, ]} onPress={() => { setSelectedNodeId(item.id); setSelectedNodeName(`${item.name || item.id.slice(0, 8)} (${item.chainName})`); setShowNodeModal(false); }}> <react_native_1.Text style={{ color: selectedNodeId === item.id ? '#fff' : theme.text }}> {item.name || item.id.slice(0, 8)} ({item.chainName}) </react_native_1.Text> </react_native_1.TouchableOpacity>)} ListEmptyComponent={<react_native_1.Text style={{ color: theme.textSecondary, textAlign: 'center', marginTop: 20 }}>No nodes available for this region.</react_native_1.Text>} style={{ maxHeight: 300 }}/> <react_native_1.TouchableOpacity style={[styles.closeButton, { backgroundColor: theme.primary, marginTop: 20 }]} onPress={() => setShowNodeModal(false)}> <react_native_1.Text style={{ color: '#fff' }}>Close</react_native_1.Text> </react_native_1.TouchableOpacity> </react_native_1.View> </react_native_1.View> </react_native_1.Modal> </>)} <react_native_1.TouchableOpacity style={[ styles.createButton, { backgroundColor: theme.primary, opacity: !newClientName || !selectedRegion || !selectedNodeId || isCreatingClient ? 0.5 : 1, }, ]} onPress={createVPNClient} disabled={!newClientName || !selectedRegion || !selectedNodeId || isCreatingClient}> {isCreatingClient ? (<react_native_1.ActivityIndicator color="#ffffff" size="small"/>) : (<react_native_1.Text style={styles.createButtonText}>Create Client</react_native_1.Text>)} </react_native_1.TouchableOpacity> </react_native_1.View> {showQrCode && configFile && (<react_native_1.View style={styles.qrCodeContainer}> <react_native_qrcode_svg_1.default value={configFile} size={200} backgroundColor={theme.surface} color={theme.text}/> <react_native_1.Text style={[styles.qrCodeText, { color: theme.textSecondary }]}> Scan this QR code with the WireGuard app </react_native_1.Text> </react_native_1.View>)} </react_native_1.View> </>)} </react_native_1.ScrollView>); }; exports.ClientCreator = ClientCreator; const styles = react_native_1.StyleSheet.create({ container: { padding: 20, borderRadius: 16, }, section: { padding: 20, borderRadius: 16, }, sectionTitle: { fontSize: 24, fontWeight: 'bold', marginBottom: 20, }, form: { gap: 16, }, label: { fontSize: 16, fontWeight: '600', marginBottom: 4, }, input: { borderWidth: 1, borderRadius: 8, padding: 12, fontSize: 16, }, pickerWrapper: { borderWidth: 1, borderColor: '#e5e7eb', borderRadius: 8, marginBottom: 16, overflow: 'hidden', }, createButton: { padding: 16, borderRadius: 12, alignItems: 'center', marginTop: 16, }, createButtonText: { color: '#ffffff', fontSize: 16, fontWeight: '600', }, qrCodeContainer: { alignItems: 'center', marginTop: 20, }, qrCodeText: { fontSize: 14, textAlign: 'center', marginTop: 16, }, regionButton: { paddingVertical: 10, paddingHorizontal: 18, borderRadius: 8, borderWidth: 1, marginRight: 10, marginBottom: 8, }, nodeSelectButton: { borderWidth: 1, borderRadius: 8, padding: 14, marginBottom: 16, alignItems: 'center', }, modalOverlay: { flex: 1, backgroundColor: 'rgba(0,0,0,0.4)', justifyContent: 'center', alignItems: 'center', }, modalContent: { width: '90%', maxWidth: 400, borderRadius: 16, padding: 20, shadowColor: '#000', shadowOffset: { width: 0, height: 2 }, shadowOpacity: 0.25, shadowRadius: 3.84, elevation: 5, }, nodeItem: { padding: 14, borderRadius: 8, borderWidth: 1, marginBottom: 10, alignItems: 'center', }, closeButton: { padding: 12, borderRadius: 8, alignItems: 'center', }, });