mirror of
https://github.com/EthanShoeDev/fressh.git
synced 2026-01-11 14:22:51 +00:00
mono repo
This commit is contained in:
268
apps/mobile/src/lib/secrets-manager.ts
Normal file
268
apps/mobile/src/lib/secrets-manager.ts
Normal file
@@ -0,0 +1,268 @@
|
||||
import SSHClient from '@dylankenneally/react-native-ssh-sftp';
|
||||
import { queryOptions } from '@tanstack/react-query';
|
||||
import * as SecureStore from 'expo-secure-store';
|
||||
import * as z from 'zod';
|
||||
import { queryClient } from './utils';
|
||||
|
||||
const keys = {
|
||||
storagePrefix: 'privateKey_',
|
||||
manifestKey: 'privateKeysManifest',
|
||||
} as const;
|
||||
|
||||
const keyManifestSchema = z.object({
|
||||
manifestVersion: z.number().default(1),
|
||||
keys: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
priority: z.number(),
|
||||
createdAt: z.date(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
async function getKeyManifest() {
|
||||
const rawManifest = await SecureStore.getItemAsync(keys.manifestKey);
|
||||
const manifest = rawManifest
|
||||
? JSON.parse(rawManifest)
|
||||
: {
|
||||
manifestVersion: 1,
|
||||
keys: [],
|
||||
};
|
||||
return keyManifestSchema.parse(manifest);
|
||||
}
|
||||
|
||||
async function savePrivateKey(params: {
|
||||
keyId: string;
|
||||
privateKey: string;
|
||||
priority: number;
|
||||
}) {
|
||||
const manifest = await getKeyManifest();
|
||||
|
||||
const existingKey = manifest.keys.find((key) => key.id === params.keyId);
|
||||
|
||||
if (existingKey) throw new Error('Key already exists');
|
||||
|
||||
const newKey = {
|
||||
id: params.keyId,
|
||||
priority: params.priority,
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
manifest.keys.push(newKey);
|
||||
await SecureStore.setItemAsync(
|
||||
`${keys.storagePrefix}${params.keyId}`,
|
||||
params.privateKey,
|
||||
);
|
||||
await SecureStore.setItemAsync(keys.manifestKey, JSON.stringify(manifest));
|
||||
await queryClient.invalidateQueries({ queryKey: [keyQueryKey] });
|
||||
}
|
||||
|
||||
async function getPrivateKey(keyId: string) {
|
||||
const manifest = await getKeyManifest();
|
||||
const key = manifest.keys.find((key) => key.id === keyId);
|
||||
if (!key) throw new Error('Key not found');
|
||||
const privateKey = await SecureStore.getItemAsync(
|
||||
`${keys.storagePrefix}${keyId}`,
|
||||
);
|
||||
if (!privateKey) throw new Error('Key not found');
|
||||
return {
|
||||
...key,
|
||||
privateKey,
|
||||
};
|
||||
}
|
||||
|
||||
async function deletePrivateKey(keyId: string) {
|
||||
const manifest = await getKeyManifest();
|
||||
const key = manifest.keys.find((key) => key.id === keyId);
|
||||
if (!key) throw new Error('Key not found');
|
||||
manifest.keys = manifest.keys.filter((key) => key.id !== keyId);
|
||||
await SecureStore.setItemAsync(keys.manifestKey, JSON.stringify(manifest));
|
||||
await SecureStore.deleteItemAsync(`${keys.storagePrefix}${keyId}`);
|
||||
await queryClient.invalidateQueries({ queryKey: [keyQueryKey] });
|
||||
}
|
||||
|
||||
const connections = {
|
||||
storagePrefix: 'connection_',
|
||||
manifestKey: 'connectionsManifest',
|
||||
} as const;
|
||||
|
||||
const connectionsManifestSchema = z.object({
|
||||
manifestVersion: z.number().default(1),
|
||||
connections: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
priority: z.number(),
|
||||
createdAt: z.date(),
|
||||
modifiedAt: z.date(),
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
export const connectionDetailsSchema = z.object({
|
||||
host: z.string().min(1),
|
||||
port: z.number().min(1),
|
||||
username: z.string().min(1),
|
||||
security: z.discriminatedUnion('type', [
|
||||
z.object({
|
||||
type: z.literal('password'),
|
||||
password: z.string().min(1),
|
||||
}),
|
||||
z.object({
|
||||
type: z.literal('key'),
|
||||
keyId: z.string().min(1),
|
||||
}),
|
||||
]),
|
||||
});
|
||||
|
||||
export type ConnectionDetails = z.infer<typeof connectionDetailsSchema>;
|
||||
|
||||
async function getConnectionManifest() {
|
||||
const rawManifest = await SecureStore.getItemAsync(connections.manifestKey);
|
||||
const manifest = rawManifest
|
||||
? JSON.parse(rawManifest)
|
||||
: {
|
||||
manifestVersion: 1,
|
||||
connections: [],
|
||||
};
|
||||
return connectionsManifestSchema.parse(manifest);
|
||||
}
|
||||
|
||||
async function upsertConnection(params: {
|
||||
id: string;
|
||||
details: ConnectionDetails;
|
||||
priority: number;
|
||||
}) {
|
||||
const manifest = await getConnectionManifest();
|
||||
const existingConnection = manifest.connections.find(
|
||||
(connection) => connection.id === params.id,
|
||||
);
|
||||
|
||||
const newConnection = existingConnection
|
||||
? {
|
||||
...existingConnection,
|
||||
priority: params.priority,
|
||||
modifiedAt: new Date(),
|
||||
}
|
||||
: {
|
||||
id: params.id,
|
||||
priority: params.priority,
|
||||
createdAt: new Date(),
|
||||
modifiedAt: new Date(),
|
||||
};
|
||||
|
||||
await SecureStore.setItemAsync(
|
||||
connections.manifestKey,
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
await SecureStore.setItemAsync(
|
||||
`${connections.storagePrefix}${params.id}`,
|
||||
JSON.stringify(params.details),
|
||||
);
|
||||
await queryClient.invalidateQueries({ queryKey: [connectionQueryKey] });
|
||||
return existingConnection ?? newConnection;
|
||||
}
|
||||
|
||||
async function deleteConnection(id: string) {
|
||||
const manifest = await getConnectionManifest();
|
||||
const connection = manifest.connections.find(
|
||||
(connection) => connection.id === id,
|
||||
);
|
||||
if (!connection) throw new Error('Connection not found');
|
||||
manifest.connections = manifest.connections.filter(
|
||||
(connection) => connection.id !== id,
|
||||
);
|
||||
await SecureStore.setItemAsync(
|
||||
connections.manifestKey,
|
||||
JSON.stringify(manifest),
|
||||
);
|
||||
await SecureStore.deleteItemAsync(`${connections.storagePrefix}${id}`);
|
||||
await queryClient.invalidateQueries({ queryKey: [connectionQueryKey] });
|
||||
}
|
||||
|
||||
async function getConnection(id: string) {
|
||||
const manifest = await getConnectionManifest();
|
||||
const connection = manifest.connections.find(
|
||||
(connection) => connection.id === id,
|
||||
);
|
||||
if (!connection) throw new Error('Connection not found');
|
||||
const detailsString = await SecureStore.getItemAsync(
|
||||
`${connections.storagePrefix}${id}`,
|
||||
);
|
||||
if (!detailsString) throw new Error('Connection details not found');
|
||||
const detailsJson = JSON.parse(detailsString);
|
||||
const details = connectionDetailsSchema.parse(detailsJson);
|
||||
return { ...connection, details };
|
||||
}
|
||||
|
||||
const connectionQueryKey = 'connections';
|
||||
|
||||
const listConnectionsQueryOptions = queryOptions({
|
||||
queryKey: [connectionQueryKey],
|
||||
queryFn: async () => {
|
||||
const manifest = await getConnectionManifest();
|
||||
const firstConnectionMeta = manifest.connections[0];
|
||||
const firstConnection = firstConnectionMeta
|
||||
? await getConnection(firstConnectionMeta.id)
|
||||
: null;
|
||||
|
||||
return {
|
||||
manifest,
|
||||
firstConnection,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const getConnectionQueryOptions = (id: string) =>
|
||||
queryOptions({
|
||||
queryKey: [connectionQueryKey, id],
|
||||
queryFn: () => getConnection(id),
|
||||
});
|
||||
|
||||
const keyQueryKey = 'keys';
|
||||
|
||||
const listKeysQueryOptions = queryOptions({
|
||||
queryKey: [keyQueryKey],
|
||||
queryFn: getKeyManifest,
|
||||
});
|
||||
|
||||
// https://github.com/dylankenneally/react-native-ssh-sftp/blob/ea55436d8d40378a8f9dabb95b463739ffb219fa/android/src/main/java/me/keeex/rnssh/RNSshClientModule.java#L101-L119
|
||||
export type SshPrivateKeyType = 'dsa' | 'rsa' | 'ecdsa' | 'ed25519' | 'ed448';
|
||||
async function generateKeyPair(params: {
|
||||
type: SshPrivateKeyType;
|
||||
passphrase?: string;
|
||||
keySize?: number;
|
||||
comment?: string;
|
||||
}) {
|
||||
const keyPair = await SSHClient.generateKeyPair(
|
||||
params.type,
|
||||
params.passphrase,
|
||||
params.keySize,
|
||||
params.comment,
|
||||
);
|
||||
return keyPair;
|
||||
}
|
||||
|
||||
export const secretsManager = {
|
||||
keys: {
|
||||
utils: {
|
||||
getKeyManifest,
|
||||
savePrivateKey,
|
||||
getPrivateKey,
|
||||
deletePrivateKey,
|
||||
generateKeyPair,
|
||||
},
|
||||
query: {
|
||||
list: listKeysQueryOptions,
|
||||
},
|
||||
},
|
||||
connections: {
|
||||
utils: {
|
||||
upsertConnection,
|
||||
deleteConnection,
|
||||
},
|
||||
query: {
|
||||
list: listConnectionsQueryOptions,
|
||||
get: getConnectionQueryOptions,
|
||||
},
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user