Simplify connection flow, mobile transfer UI, and header actions

This commit is contained in:
2026-10-12 18:14:54 +09:00
parent 5f431ee153
commit 4f0231da3d
22 changed files with 820 additions and 131 deletions

View File

@@ -331,6 +331,7 @@ export async function createApp(cfg: Config, options: { mailer?: Mailer } = {})
device: service.device(g.id),
devices: service.peers(g),
sameNetworkDiscoveryEnabled: cfg.discoveryEnabled,
spaceInviteCodeLength: cfg.spaceInviteCodeLength,
locationDiscovery: {
enabled: cfg.locationDiscoveryEnabled,
radiusMeters: cfg.locationRadiusMeters,

View File

@@ -18,6 +18,7 @@ export type Config = {
requestSeconds: number;
uploadStartHours: number;
spaceMinutes: number;
spaceInviteCodeLength: number;
spaceMax: number;
offlineGraceMs: number;
maxUrlBytes: number;
@@ -67,6 +68,9 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
throw Error('PROXY_DIAGNOSTIC_TOKEN must contain at least 32 characters');
if (num('LOCATION_DISCOVERY_RADIUS_METERS', 100) > 1000)
throw Error('LOCATION_DISCOVERY_RADIUS_METERS must not exceed 1000');
const spaceInviteCodeLength = num('SPACE_INVITE_CODE_LENGTH', 4);
if (![4, 6].includes(spaceInviteCodeLength))
throw Error('SPACE_INVITE_CODE_LENGTH must be 4 or 6');
return {
production,
appUrl,
@@ -92,6 +96,7 @@ export function loadConfig(env: NodeJS.ProcessEnv = process.env): Config {
requestSeconds: num('LOCAL_REQUEST_TIMEOUT_SECONDS', 60),
uploadStartHours: num('ACCEPTED_UPLOAD_START_HOURS', 24),
spaceMinutes: num('SPACE_RETENTION_MINUTES', 10),
spaceInviteCodeLength,
spaceMax: num('SPACE_MAX_DEVICES', 10),
offlineGraceMs: num('OFFLINE_GRACE_MS', 3000, 0),
maxUrlBytes: num('MAX_URL_PAYLOAD_SIZE_KB', 1024) * 1024,

View File

@@ -0,0 +1,10 @@
import { randomInt } from 'node:crypto';
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
export function newInviteCode(length, isTaken, random = randomInt) {
for (let attempt = 0; attempt < 32; attempt++) {
const candidate = Array.from({ length }, () => alphabet[random(alphabet.length)]).join('');
if (!isTaken(candidate)) return candidate;
}
return null;
}

View File

@@ -5,7 +5,8 @@ import { Storage } from './storage.js';
import { ResumableUploads } from './resumable.js';
import { NearbyLocations } from './nearby.js';
import { Vault } from './vault.js';
import { randomInt, randomUUID } from 'node:crypto';
import { randomUUID } from 'node:crypto';
import { newInviteCode } from './inviteCode.js';
import { and, eq, gt, isNull, or, desc, lt, inArray, sql } from 'drizzle-orm';
import type { DB } from './db/index.js';
import * as s from './db/schema.js';
@@ -380,8 +381,18 @@ export class Service {
const now = Date.now(),
id = randomUUID(),
token = randomToken();
const alphabet = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
const code = Array.from({ length: 8 }, () => alphabet[randomInt(alphabet.length)]).join('');
// The unique DB constraint includes historical spaces; never reuse their codes.
const code =
newInviteCode(this.cfg.spaceInviteCodeLength, (candidate: string) =>
Boolean(
this.db
.select({ id: s.spaces.id })
.from(s.spaces)
.where(eq(s.spaces.codeHash, hmac(this.cfg.discoverySecret, candidate)))
.get(),
),
) ??
fail(503, 'SPACE_CODE_BUSY', '초대 코드를 만들지 못했습니다. 잠시 후 다시 시도해 주세요.');
this.db.transaction(() => {
this.db
.insert(s.spaces)