feat(web): add account workspaces and signed content flows
This commit is contained in:
1 parent
7b8d61d1ba
commit
b5cf3d5958
117 files changed
+2789
-1674
No files matched your search
@@ -1,9 +1,13 @@
|
||||
import { ChannelSession, toBase64Url } from '@webdrop/core';
|
||||
import { getResource, listResources, removeResource, saveResource, type SavedResource } from '../database';
|
||||
import { cacheResource, getResource, listResources, removeResource, saveResource, type SavedResource } from '../database';
|
||||
import type { ParsedChannelLink } from '@/lib/channelLink';
|
||||
|
||||
import type { ChannelInfo, ChannelSettingsPatch, CreateOptions } from '../types';
|
||||
import { apiFetch, readJson } from './http';
|
||||
import { apiFetch, readJson, getServerInfo } from './http';
|
||||
import { channelCache, isOfflineFailure } from './resourceMetadata';
|
||||
import { accountToken } from './accountSession';
|
||||
import { attachDataKeys, initializeChannelKeys } from './channelDataKeys';
|
||||
import type { ChannelSettingsPatch as SettingsRequest, SeatJoined, MemberBody } from '../generated/api';
|
||||
import { getSession, sessions } from './sessions';
|
||||
import { deviceIdentity } from './presentation';
|
||||
import { makeLink } from './links';
|
||||
@@ -13,26 +17,26 @@ import { publishWorkspace } from './presentation';
|
||||
import { channelMetadata } from './channelEvents';
|
||||
import { requireAccount, accountPublicKey } from './accountSession';
|
||||
import { sealChannelGrant } from './channelGrant';
|
||||
import { createThread } from './channelThreads';
|
||||
|
||||
|
||||
|
||||
export async function createChannel(options: CreateOptions, captchaToken?: string): Promise<ChannelInfo> {
|
||||
const username = requireAccount();
|
||||
const defaults = (await getServerInfo()).defaults?.channel;
|
||||
if (!defaults) throw new Error('服务端需要升级以提供统一设置');
|
||||
if (defaults.join_policy !== 'open' && defaults.join_policy !== 'invite') throw new Error('空间加入规则无效');
|
||||
const endpointSecret = crypto.getRandomValues(new Uint8Array(32));
|
||||
const { session, request } = await ChannelSession.create({
|
||||
mode: options.mode,
|
||||
endpointSecret,
|
||||
settings: {
|
||||
...defaults,
|
||||
name: options.name?.trim() || undefined,
|
||||
seats: options.seats,
|
||||
ttl_seconds: options.ttlSeconds,
|
||||
members_can_upload: options.membersCanUpload,
|
||||
delete_policy: 'own',
|
||||
max_items: 1000,
|
||||
file_max_downloads: options.fileMaxDownloads ?? 10,
|
||||
ui_restrict_copy_download: false,
|
||||
join_policy: options.joinPolicy ?? 'invite', allow_guests: options.allowGuests ?? false,
|
||||
file_max_downloads: options.fileMaxDownloads,
|
||||
join_policy: options.joinPolicy ?? defaults.join_policy, allow_guests: options.allowGuests ?? defaults.allow_guests,
|
||||
},
|
||||
});
|
||||
try {
|
||||
@@ -42,13 +46,14 @@ export async function createChannel(options: CreateOptions, captchaToken?: strin
|
||||
}).then((response) => response.json() as Promise<ChannelWire>);
|
||||
session.verifyChannelInfo(info);
|
||||
const resource: SavedResource = {
|
||||
...channelCache(info),
|
||||
id: session.id,
|
||||
creatorAccount: info.creator_account,
|
||||
creatorAccount: info.creator_account ?? undefined,
|
||||
kind: 'channel', mode: session.mode, role: 'owner',
|
||||
name: info.settings.name,
|
||||
name: info.settings.name ?? undefined,
|
||||
link: session.link(self.location.origin), ownerLink: session.link(self.location.origin, 'owner'),
|
||||
endpointSecret: Uint8Array.from(session.endpointSecret), createdAt: info.created_at, expiresAt: info.expires_at,
|
||||
seats: info.settings.seats, membersCanUpload: info.settings.members_can_upload,
|
||||
seats: info.settings.seats,
|
||||
maxDownloads: info.settings.file_max_downloads, localItems: [],
|
||||
};
|
||||
const ownerLabel = await session.sealLabel((await deviceIdentity(session.endpointSecret)).name);
|
||||
@@ -57,7 +62,9 @@ export async function createChannel(options: CreateOptions, captchaToken?: strin
|
||||
});
|
||||
await saveResource(resource);
|
||||
sessions.set(session.id, session);
|
||||
await createThread(session.id, '常规');
|
||||
attachDataKeys(session, resource);
|
||||
const workspace = await import('./workspaceStructure').then(module => module.loadWorkspace(session.id));
|
||||
if (workspace.structure.defaultChannelId) await initializeChannelKeys(resource, session, workspace.structure.defaultChannelId);
|
||||
return toChannelInfo(info, resource);
|
||||
} catch (error) {
|
||||
session.dispose();
|
||||
@@ -75,15 +82,16 @@ export async function joinChannel(input: { link: ParsedChannelLink; password?: s
|
||||
session.verifyChannelInfo(info);
|
||||
const joined = await apiFetch(`/channels/${id}/seats`, {
|
||||
method: 'POST', headers: { ...session.authHeaders(session.can('owner') ? 'owner' : 'read'), 'Content-Type': 'application/json' }, body: JSON.stringify({ password: input.password }),
|
||||
}).then((response) => response.json() as Promise<{ seats_used: number }>);
|
||||
}).then((response) => response.json() as Promise<SeatJoined>);
|
||||
info.seats_used = joined.seats_used;
|
||||
const resource: SavedResource = {
|
||||
...channelCache(info),
|
||||
id, kind: 'channel', mode: session.mode, role: session.role,
|
||||
creatorAccount: info.creator_account,
|
||||
name: info.settings.name,
|
||||
creatorAccount: info.creator_account ?? undefined,
|
||||
name: info.settings.name ?? undefined,
|
||||
link: session.link(self.location.origin), ownerLink: session.can('owner') ? session.link(self.location.origin, 'owner') : undefined,
|
||||
endpointSecret: Uint8Array.from(session.endpointSecret), createdAt: info.created_at, expiresAt: info.expires_at,
|
||||
seats: info.settings.seats, membersCanUpload: info.settings.members_can_upload,
|
||||
seats: info.settings.seats,
|
||||
maxDownloads: info.settings.file_max_downloads, localItems: existing?.localItems ?? [],
|
||||
};
|
||||
const label = await session.sealLabel((await deviceIdentity(session.endpointSecret)).name);
|
||||
@@ -106,28 +114,52 @@ export async function previewChannel(link: ParsedChannelLink): Promise<ChannelIn
|
||||
session.verifyChannelInfo(info);
|
||||
return toChannelInfo(info, { id: link.id, kind: 'channel', mode: session.mode, role: session.role,
|
||||
endpointSecret: session.endpointSecret, link: session.link(self.location.origin), createdAt: info.created_at,
|
||||
expiresAt: info.expires_at, seats: info.settings.seats, membersCanUpload: info.settings.members_can_upload });
|
||||
expiresAt: info.expires_at, seats: info.settings.seats });
|
||||
} finally { session.dispose(); }
|
||||
}
|
||||
|
||||
export async function channelMembers(id: string): Promise<unknown> {
|
||||
export async function channelMembers(id: string): Promise<MemberBody[]> {
|
||||
const { session } = await getSession(id);
|
||||
return readJson(`/channels/${id}/members`, session.authHeaders(session.can('owner') ? 'owner' : 'read'));
|
||||
}
|
||||
|
||||
export async function listChannels(): Promise<ChannelInfo[]> {
|
||||
const saved = await listResources('channel');
|
||||
return saved.map((resource) => ({
|
||||
let catalog: Map<string, ChannelWire> | undefined;
|
||||
if (accountToken()) {
|
||||
try {
|
||||
const rows = await apiFetch('/accounts/spaces').then(r => r.json() as Promise<ChannelWire[]>);
|
||||
catalog = new Map(rows.map(row => [row.id, row]));
|
||||
} catch (error) { if (!isOfflineFailure(error)) throw error; }
|
||||
}
|
||||
const rows = await Promise.all(saved.map(async (resource): Promise<ChannelInfo | null> => {
|
||||
try {
|
||||
if (catalog && !catalog.has(resource.id)) return null;
|
||||
const { session } = await getSession(resource.id);
|
||||
const raw = catalog?.get(resource.id) ?? await readJson<ChannelWire>(`/channels/${resource.id}`, session.authHeaders(session.can('owner') ? 'owner' : session.can('write') ? 'write' : 'read'));
|
||||
session.verifyChannelInfo(raw);
|
||||
const info = toChannelInfo(raw, resource);
|
||||
await cacheResource(resource, channelCache(raw));
|
||||
return { ...info, dataSource: 'server' };
|
||||
} catch (error) {
|
||||
if (error instanceof Error && 'status' in error && (error.status === 404 || error.status === 410)) return null;
|
||||
if (!isOfflineFailure(error)) throw error;
|
||||
if (resource.channelMetadata) return { ...toChannelInfo(resource.channelMetadata, resource), dataSource: 'cache' };
|
||||
return {
|
||||
id: resource.id, name: resource.name, mode: resource.mode, role: resource.role, seats: resource.seats, maxDownloads: resource.maxDownloads ?? undefined,
|
||||
creatorAccount: resource.creatorAccount,
|
||||
memberLink: resource.link, ownerLink: resource.ownerLink, createdAt: resource.createdAt, expiresAt: resource.expiresAt,
|
||||
dataSource: 'cache',
|
||||
};
|
||||
}
|
||||
}));
|
||||
return rows.filter((row): row is ChannelInfo => row !== null);
|
||||
}
|
||||
|
||||
export async function updateChannelSettings(resourceId: string, patch: ChannelSettingsPatch): Promise<void> {
|
||||
const { session, resource } = await getSession(resourceId);
|
||||
if (!session.can('owner')) throw new Error('只有所有者可以修改空间设置');
|
||||
const body: Record<string, unknown> = {};
|
||||
const body: SettingsRequest = {};
|
||||
if (patch.name !== undefined) body.name = patch.name.trim();
|
||||
if (patch.seats !== undefined) body.seats = patch.seats;
|
||||
if (patch.membersCanUpload !== undefined) body.members_can_upload = patch.membersCanUpload;
|
||||
@@ -141,13 +173,8 @@ export async function updateChannelSettings(resourceId: string, patch: ChannelSe
|
||||
body: JSON.stringify(body),
|
||||
}).then((response) => response.json() as Promise<ChannelWire>);
|
||||
session.verifyChannelInfo(info);
|
||||
resource.name = info.settings.name;
|
||||
resource.seats = info.settings.seats;
|
||||
resource.membersCanUpload = info.settings.members_can_upload;
|
||||
resource.maxDownloads = info.settings.file_max_downloads;
|
||||
resource.expiresAt = info.expires_at;
|
||||
await saveResource(resource);
|
||||
publishWorkspace(resourceId, { t: 'channel', info: toChannelInfo(info, resource) });
|
||||
const updated = await cacheResource(resource, channelCache(info));
|
||||
publishWorkspace(resourceId, { t: 'channel', info: toChannelInfo(info, updated) });
|
||||
}
|
||||
|
||||
export async function renameDevice(resourceId: string, endpointHash: string, name: string): Promise<void> {
|
||||
|
||||
Reference in new issue
Block a user