- Rename from @lossless.zone/s3container to @lossless.zone/objectstorage - Replace @push.rocks/smarts3 with @push.rocks/smartstorage - Change env var prefix from S3_ to OBJST_ - Rename S3Container class to ObjectStorageContainer - Update web component prefix from s3c- to objst- - Update UI labels, CLI flags, documentation, and Docker config
590 lines
20 KiB
TypeScript
590 lines
20 KiB
TypeScript
import * as plugins from './plugins.js';
|
|
import * as interfaces from '../ts_interfaces/index.js';
|
|
|
|
// ============================================================================
|
|
// Smartstate instance
|
|
// ============================================================================
|
|
export const appState = new plugins.domtools.plugins.smartstate.Smartstate();
|
|
|
|
// ============================================================================
|
|
// State Part Interfaces
|
|
// ============================================================================
|
|
|
|
export interface ILoginState {
|
|
identity: interfaces.data.IIdentity | null;
|
|
isLoggedIn: boolean;
|
|
}
|
|
|
|
export interface IServerState {
|
|
status: interfaces.data.IServerStatus | null;
|
|
connectionInfo: interfaces.data.IConnectionInfo | null;
|
|
}
|
|
|
|
export interface IBucketsState {
|
|
buckets: interfaces.data.IBucketInfo[];
|
|
}
|
|
|
|
export interface IObjectsState {
|
|
result: interfaces.data.IObjectListResult | null;
|
|
currentBucket: string;
|
|
currentPrefix: string;
|
|
}
|
|
|
|
export interface ICredentialsState {
|
|
credentials: interfaces.data.IObjstCredential[];
|
|
}
|
|
|
|
export interface IPoliciesState {
|
|
policies: interfaces.data.INamedPolicy[];
|
|
}
|
|
|
|
export interface IConfigState {
|
|
config: interfaces.data.IServerConfig | null;
|
|
}
|
|
|
|
export interface IUiState {
|
|
activeView: string;
|
|
autoRefresh: boolean;
|
|
refreshInterval: number;
|
|
}
|
|
|
|
// ============================================================================
|
|
// State Parts
|
|
// ============================================================================
|
|
|
|
export const loginStatePart = await appState.getStatePart<ILoginState>(
|
|
'login',
|
|
{
|
|
identity: null,
|
|
isLoggedIn: false,
|
|
},
|
|
'persistent',
|
|
);
|
|
|
|
export const serverStatePart = await appState.getStatePart<IServerState>(
|
|
'server',
|
|
{
|
|
status: null,
|
|
connectionInfo: null,
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const bucketsStatePart = await appState.getStatePart<IBucketsState>(
|
|
'buckets',
|
|
{
|
|
buckets: [],
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const objectsStatePart = await appState.getStatePart<IObjectsState>(
|
|
'objects',
|
|
{
|
|
result: null,
|
|
currentBucket: '',
|
|
currentPrefix: '',
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const credentialsStatePart = await appState.getStatePart<ICredentialsState>(
|
|
'credentials',
|
|
{
|
|
credentials: [],
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const policiesStatePart = await appState.getStatePart<IPoliciesState>(
|
|
'policies',
|
|
{
|
|
policies: [],
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const configStatePart = await appState.getStatePart<IConfigState>(
|
|
'config',
|
|
{
|
|
config: null,
|
|
},
|
|
'soft',
|
|
);
|
|
|
|
export const uiStatePart = await appState.getStatePart<IUiState>(
|
|
'ui',
|
|
{
|
|
activeView: 'overview',
|
|
autoRefresh: true,
|
|
refreshInterval: 30000,
|
|
},
|
|
);
|
|
|
|
// ============================================================================
|
|
// Helpers
|
|
// ============================================================================
|
|
|
|
interface IActionContext {
|
|
identity: interfaces.data.IIdentity | null;
|
|
}
|
|
|
|
const getActionContext = (): IActionContext => {
|
|
return { identity: loginStatePart.getState().identity };
|
|
};
|
|
|
|
// ============================================================================
|
|
// Login Actions
|
|
// ============================================================================
|
|
|
|
export const loginAction = loginStatePart.createAction<{
|
|
username: string;
|
|
password: string;
|
|
}>(async (statePartArg, dataArg) => {
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_AdminLoginWithUsernameAndPassword
|
|
>('/typedrequest', 'adminLoginWithUsernameAndPassword');
|
|
|
|
const response = await typedRequest.fire({
|
|
username: dataArg.username,
|
|
password: dataArg.password,
|
|
});
|
|
|
|
return {
|
|
identity: response.identity || null,
|
|
isLoggedIn: !!response.identity,
|
|
};
|
|
} catch (err) {
|
|
console.error('Login failed:', err);
|
|
return { identity: null, isLoggedIn: false };
|
|
}
|
|
});
|
|
|
|
export const logoutAction = loginStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
if (context.identity) {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_AdminLogout
|
|
>('/typedrequest', 'adminLogout');
|
|
await typedRequest.fire({ identity: context.identity });
|
|
}
|
|
} catch (err) {
|
|
console.error('Logout error:', err);
|
|
}
|
|
return { identity: null, isLoggedIn: false };
|
|
});
|
|
|
|
// ============================================================================
|
|
// Server Status Actions
|
|
// ============================================================================
|
|
|
|
export const fetchServerStatusAction = serverStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetServerStatus
|
|
>('/typedrequest', 'getServerStatus');
|
|
const response = await typedRequest.fire({ identity: context.identity! });
|
|
return { status: response.status, connectionInfo: response.connectionInfo };
|
|
} catch (err) {
|
|
console.error('Failed to fetch server status:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// Buckets Actions
|
|
// ============================================================================
|
|
|
|
export const fetchBucketsAction = bucketsStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListBuckets
|
|
>('/typedrequest', 'listBuckets');
|
|
const response = await typedRequest.fire({ identity: context.identity! });
|
|
return { buckets: response.buckets };
|
|
} catch (err) {
|
|
console.error('Failed to fetch buckets:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const createBucketAction = bucketsStatePart.createAction<{ bucketName: string }>(
|
|
async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_CreateBucket
|
|
>('/typedrequest', 'createBucket');
|
|
await typedRequest.fire({ identity: context.identity!, bucketName: dataArg.bucketName });
|
|
// Re-fetch buckets list
|
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListBuckets
|
|
>('/typedrequest', 'listBuckets');
|
|
const listResp = await listReq.fire({ identity: context.identity! });
|
|
return { buckets: listResp.buckets };
|
|
} catch (err) {
|
|
console.error('Failed to create bucket:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
},
|
|
);
|
|
|
|
export const deleteBucketAction = bucketsStatePart.createAction<{ bucketName: string }>(
|
|
async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_DeleteBucket
|
|
>('/typedrequest', 'deleteBucket');
|
|
await typedRequest.fire({ identity: context.identity!, bucketName: dataArg.bucketName });
|
|
const state = statePartArg.getState();
|
|
return {
|
|
buckets: state.buckets.filter((b) => b.name !== dataArg.bucketName),
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to delete bucket:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
},
|
|
);
|
|
|
|
// ============================================================================
|
|
// Named Policies Actions
|
|
// ============================================================================
|
|
|
|
export const fetchPoliciesAction = policiesStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListNamedPolicies
|
|
>('/typedrequest', 'listNamedPolicies');
|
|
const response = await typedRequest.fire({ identity: context.identity! });
|
|
return { policies: response.policies };
|
|
} catch (err) {
|
|
console.error('Failed to fetch policies:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const createPolicyAction = policiesStatePart.createAction<{
|
|
name: string;
|
|
description: string;
|
|
statements: interfaces.data.IObjstStatement[];
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_CreateNamedPolicy
|
|
>('/typedrequest', 'createNamedPolicy');
|
|
await typedRequest.fire({
|
|
identity: context.identity!,
|
|
name: dataArg.name,
|
|
description: dataArg.description,
|
|
statements: dataArg.statements,
|
|
});
|
|
// Re-fetch
|
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListNamedPolicies
|
|
>('/typedrequest', 'listNamedPolicies');
|
|
const listResp = await listReq.fire({ identity: context.identity! });
|
|
return { policies: listResp.policies };
|
|
} catch (err) {
|
|
console.error('Failed to create policy:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const updatePolicyAction = policiesStatePart.createAction<{
|
|
policyId: string;
|
|
name: string;
|
|
description: string;
|
|
statements: interfaces.data.IObjstStatement[];
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_UpdateNamedPolicy
|
|
>('/typedrequest', 'updateNamedPolicy');
|
|
await typedRequest.fire({
|
|
identity: context.identity!,
|
|
policyId: dataArg.policyId,
|
|
name: dataArg.name,
|
|
description: dataArg.description,
|
|
statements: dataArg.statements,
|
|
});
|
|
// Re-fetch
|
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListNamedPolicies
|
|
>('/typedrequest', 'listNamedPolicies');
|
|
const listResp = await listReq.fire({ identity: context.identity! });
|
|
return { policies: listResp.policies };
|
|
} catch (err) {
|
|
console.error('Failed to update policy:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const deletePolicyAction = policiesStatePart.createAction<{ policyId: string }>(
|
|
async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_DeleteNamedPolicy
|
|
>('/typedrequest', 'deleteNamedPolicy');
|
|
await typedRequest.fire({ identity: context.identity!, policyId: dataArg.policyId });
|
|
const state = statePartArg.getState();
|
|
return { policies: state.policies.filter((p) => p.id !== dataArg.policyId) };
|
|
} catch (err) {
|
|
console.error('Failed to delete policy:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
},
|
|
);
|
|
|
|
// Standalone async functions for policy-bucket management
|
|
|
|
export const getBucketNamedPolicies = async (bucketName: string) => {
|
|
const context = getActionContext();
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetBucketNamedPolicies
|
|
>('/typedrequest', 'getBucketNamedPolicies');
|
|
return await typedRequest.fire({ identity: context.identity!, bucketName });
|
|
};
|
|
|
|
export const attachPolicyToBucket = async (policyId: string, bucketName: string) => {
|
|
const context = getActionContext();
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_AttachPolicyToBucket
|
|
>('/typedrequest', 'attachPolicyToBucket');
|
|
return await typedRequest.fire({ identity: context.identity!, policyId, bucketName });
|
|
};
|
|
|
|
export const detachPolicyFromBucket = async (policyId: string, bucketName: string) => {
|
|
const context = getActionContext();
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_DetachPolicyFromBucket
|
|
>('/typedrequest', 'detachPolicyFromBucket');
|
|
return await typedRequest.fire({ identity: context.identity!, policyId, bucketName });
|
|
};
|
|
|
|
export const getPolicyBuckets = async (policyId: string) => {
|
|
const context = getActionContext();
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetPolicyBuckets
|
|
>('/typedrequest', 'getPolicyBuckets');
|
|
return await typedRequest.fire({ identity: context.identity!, policyId });
|
|
};
|
|
|
|
export const setPolicyBuckets = async (policyId: string, bucketNames: string[]) => {
|
|
const context = getActionContext();
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_SetPolicyBuckets
|
|
>('/typedrequest', 'setPolicyBuckets');
|
|
return await typedRequest.fire({ identity: context.identity!, policyId, bucketNames });
|
|
};
|
|
|
|
// ============================================================================
|
|
// Objects Actions
|
|
// ============================================================================
|
|
|
|
export const fetchObjectsAction = objectsStatePart.createAction<{
|
|
bucketName: string;
|
|
prefix?: string;
|
|
delimiter?: string;
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListObjects
|
|
>('/typedrequest', 'listObjects');
|
|
const response = await typedRequest.fire({
|
|
identity: context.identity!,
|
|
bucketName: dataArg.bucketName,
|
|
prefix: dataArg.prefix || '',
|
|
delimiter: dataArg.delimiter || '/',
|
|
});
|
|
return {
|
|
result: response.result,
|
|
currentBucket: dataArg.bucketName,
|
|
currentPrefix: dataArg.prefix || '',
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to fetch objects:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const deleteObjectAction = objectsStatePart.createAction<{
|
|
bucketName: string;
|
|
key: string;
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_DeleteObject
|
|
>('/typedrequest', 'deleteObject');
|
|
await typedRequest.fire({
|
|
identity: context.identity!,
|
|
bucketName: dataArg.bucketName,
|
|
key: dataArg.key,
|
|
});
|
|
// Re-fetch objects
|
|
const state = statePartArg.getState();
|
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_ListObjects
|
|
>('/typedrequest', 'listObjects');
|
|
const listResp = await listReq.fire({
|
|
identity: context.identity!,
|
|
bucketName: state.currentBucket,
|
|
prefix: state.currentPrefix,
|
|
delimiter: '/',
|
|
});
|
|
return {
|
|
result: listResp.result,
|
|
currentBucket: state.currentBucket,
|
|
currentPrefix: state.currentPrefix,
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to delete object:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// Credentials Actions
|
|
// ============================================================================
|
|
|
|
export const fetchCredentialsAction = credentialsStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetCredentials
|
|
>('/typedrequest', 'getCredentials');
|
|
const response = await typedRequest.fire({ identity: context.identity! });
|
|
return { credentials: response.credentials };
|
|
} catch (err) {
|
|
console.error('Failed to fetch credentials:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const addCredentialAction = credentialsStatePart.createAction<{
|
|
accessKeyId: string;
|
|
secretAccessKey: string;
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_AddCredential
|
|
>('/typedrequest', 'addCredential');
|
|
await typedRequest.fire({
|
|
identity: context.identity!,
|
|
accessKeyId: dataArg.accessKeyId,
|
|
secretAccessKey: dataArg.secretAccessKey,
|
|
});
|
|
// Re-fetch credentials
|
|
const listReq = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetCredentials
|
|
>('/typedrequest', 'getCredentials');
|
|
const listResp = await listReq.fire({ identity: context.identity! });
|
|
return { credentials: listResp.credentials };
|
|
} catch (err) {
|
|
console.error('Failed to add credential:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
export const removeCredentialAction = credentialsStatePart.createAction<{
|
|
accessKeyId: string;
|
|
}>(async (statePartArg, dataArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_RemoveCredential
|
|
>('/typedrequest', 'removeCredential');
|
|
await typedRequest.fire({
|
|
identity: context.identity!,
|
|
accessKeyId: dataArg.accessKeyId,
|
|
});
|
|
const state = statePartArg.getState();
|
|
return {
|
|
credentials: state.credentials.filter((c) => c.accessKeyId !== dataArg.accessKeyId),
|
|
};
|
|
} catch (err) {
|
|
console.error('Failed to remove credential:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// Config Actions
|
|
// ============================================================================
|
|
|
|
export const fetchConfigAction = configStatePart.createAction(async (statePartArg) => {
|
|
const context = getActionContext();
|
|
try {
|
|
const typedRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
|
|
interfaces.requests.IReq_GetServerConfig
|
|
>('/typedrequest', 'getServerConfig');
|
|
const response = await typedRequest.fire({ identity: context.identity! });
|
|
return { config: response.config };
|
|
} catch (err) {
|
|
console.error('Failed to fetch config:', err);
|
|
return statePartArg.getState();
|
|
}
|
|
});
|
|
|
|
// ============================================================================
|
|
// UI Actions
|
|
// ============================================================================
|
|
|
|
export const setActiveViewAction = uiStatePart.createAction<{ view: string }>(
|
|
async (statePartArg, dataArg) => {
|
|
return { ...statePartArg.getState(), activeView: dataArg.view };
|
|
},
|
|
);
|
|
|
|
// ============================================================================
|
|
// Auto-refresh system
|
|
// ============================================================================
|
|
|
|
let refreshIntervalHandle: ReturnType<typeof setInterval> | null = null;
|
|
|
|
const dispatchCombinedRefreshAction = async () => {
|
|
const loginState = loginStatePart.getState();
|
|
if (!loginState.isLoggedIn) return;
|
|
|
|
try {
|
|
await serverStatePart.dispatchAction(fetchServerStatusAction, null);
|
|
} catch (_err) {
|
|
// Silently fail on auto-refresh
|
|
}
|
|
};
|
|
|
|
const startAutoRefresh = () => {
|
|
const uiState = uiStatePart.getState();
|
|
const loginState = loginStatePart.getState();
|
|
|
|
if (uiState.autoRefresh && loginState.isLoggedIn) {
|
|
if (refreshIntervalHandle) {
|
|
clearInterval(refreshIntervalHandle);
|
|
}
|
|
refreshIntervalHandle = setInterval(() => {
|
|
dispatchCombinedRefreshAction();
|
|
}, uiState.refreshInterval);
|
|
} else {
|
|
if (refreshIntervalHandle) {
|
|
clearInterval(refreshIntervalHandle);
|
|
refreshIntervalHandle = null;
|
|
}
|
|
}
|
|
};
|
|
|
|
uiStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
|
loginStatePart.select((s) => s).subscribe(() => startAutoRefresh());
|
|
startAutoRefresh();
|