diff --git a/electron/app/deep-links.ts b/electron/app/deep-links.ts index 2991ac0..e610b39 100644 --- a/electron/app/deep-links.ts +++ b/electron/app/deep-links.ts @@ -4,9 +4,24 @@ import { createWindow, getMainWindow } from '../window/create-window'; const CUSTOM_PROTOCOL = 'toju'; const DEEP_LINK_PREFIX = `${CUSTOM_PROTOCOL}://`; +const DEV_SINGLE_INSTANCE_EXIT_CODE_ENV = 'METOYOU_SINGLE_INSTANCE_EXIT_CODE'; let pendingDeepLink: string | null = null; +function resolveDevSingleInstanceExitCode(): number | null { + const rawValue = process.env[DEV_SINGLE_INSTANCE_EXIT_CODE_ENV]; + + if (!rawValue) { + return null; + } + + const parsedValue = Number.parseInt(rawValue, 10); + + return Number.isInteger(parsedValue) && parsedValue > 0 + ? parsedValue + : null; +} + function extractDeepLink(argv: string[]): string | null { return argv.find((argument) => typeof argument === 'string' && argument.startsWith(DEEP_LINK_PREFIX)) || null; } @@ -60,7 +75,14 @@ export function initializeDeepLinkHandling(): boolean { const hasSingleInstanceLock = app.requestSingleInstanceLock(); if (!hasSingleInstanceLock) { - app.quit(); + const devExitCode = resolveDevSingleInstanceExitCode(); + + if (devExitCode != null) { + app.exit(devExitCode); + } else { + app.quit(); + } + return false; } diff --git a/server/src/routes/invite-utils.ts b/server/src/routes/invite-utils.ts index a01c5fd..0751670 100644 --- a/server/src/routes/invite-utils.ts +++ b/server/src/routes/invite-utils.ts @@ -4,6 +4,10 @@ function buildOrigin(protocol: string, host: string): string { return `${protocol}://${host}`.replace(/\/+$/, ''); } +function originFromUrl(url: URL): string { + return buildOrigin(url.protocol.replace(':', ''), url.host); +} + export function getRequestOrigin(request: Request): string { const forwardedProtoHeader = request.get('x-forwarded-proto'); const forwardedHostHeader = request.get('x-forwarded-host'); @@ -15,18 +19,24 @@ export function getRequestOrigin(request: Request): string { export function deriveWebAppOrigin(signalOrigin: string): string { const url = new URL(signalOrigin); - const host = url.host; - if (host === 'signal.toju.app') { + if (url.hostname === 'signal.toju.app' && !url.port) { return 'https://web.toju.app'; } - if (host.startsWith('signal.')) { - return buildOrigin(url.protocol.replace(':', ''), host.replace(/^signal\./, 'web.')); + if (url.hostname.startsWith('signal.')) { + url.hostname = url.hostname.replace(/^signal\./, 'web.'); + + if (url.port === '3001') { + url.port = '4200'; + } + + return originFromUrl(url); } - if (['localhost:3001', '127.0.0.1:3001'].includes(host)) { - return buildOrigin(url.protocol.replace(':', ''), host.replace(/:3001$/, ':4200')); + if (url.port === '3001') { + url.port = '4200'; + return originFromUrl(url); } return 'https://web.toju.app'; diff --git a/server/src/routes/servers.ts b/server/src/routes/servers.ts index ea8ea0f..8a6fbf0 100644 --- a/server/src/routes/servers.ts +++ b/server/src/routes/servers.ts @@ -14,6 +14,7 @@ import { buildSignalingUrl, createServerInvite, joinServerWithAccess, + leaveServerUser, passwordHashForInput, ServerAccessError, kickServerUser, @@ -341,6 +342,24 @@ router.post('/:id/moderation/unban', async (req, res) => { res.json({ ok: true }); }); +router.post('/:id/leave', async (req, res) => { + const { id: serverId } = req.params; + const { userId } = req.body; + const server = await getServerById(serverId); + + if (!server) { + return res.status(404).json({ error: 'Server not found', errorCode: 'SERVER_NOT_FOUND' }); + } + + if (!userId) { + return res.status(400).json({ error: 'Missing userId', errorCode: 'MISSING_USER' }); + } + + await leaveServerUser(serverId, String(userId)); + + res.json({ ok: true }); +}); + router.post('/:id/heartbeat', async (req, res) => { const { id } = req.params; const { currentUsers } = req.body; diff --git a/server/src/services/server-access.service.ts b/server/src/services/server-access.service.ts index d7a7df1..805a20b 100644 --- a/server/src/services/server-access.service.ts +++ b/server/src/services/server-access.service.ts @@ -63,6 +63,10 @@ function normalizePassword(password?: string | null): string | null { return normalized.length > 0 ? normalized : null; } +function isServerOwner(server: ServerEntity, userId: string): boolean { + return server.ownerId === userId; +} + export function hashServerPassword(password: string): string { return crypto.createHash('sha256').update(password) .digest('hex'); @@ -231,6 +235,18 @@ export async function joinServerWithAccess(options: { throw new ServerAccessError(403, 'BANNED', 'Banned users cannot join this server'); } + if (isServerOwner(server, options.userId)) { + const existingMembership = await findServerMembership(server.id, options.userId); + + await ensureServerMembership(server.id, options.userId); + + return { + joinedBefore: !!existingMembership, + server: rowToServer(server), + via: 'membership' + }; + } + if (options.inviteId) { const inviteBundle = await getActiveServerInvite(options.inviteId); @@ -305,6 +321,11 @@ export async function authorizeWebSocketJoin(serverId: string, userId: string): reason: 'BANNED' }; } + if (isServerOwner(server, userId)) { + await ensureServerMembership(serverId, userId); + return { allowed: true }; + } + const membership = await findServerMembership(serverId, userId); if (membership) { @@ -327,6 +348,10 @@ export async function kickServerUser(serverId: string, userId: string): Promise< await removeServerMembership(serverId, userId); } +export async function leaveServerUser(serverId: string, userId: string): Promise { + await removeServerMembership(serverId, userId); +} + export async function banServerUser(options: BanServerUserOptions): Promise { await removeServerMembership(options.serverId, options.userId); diff --git a/src/app/core/services/server-directory.service.ts b/src/app/core/services/server-directory.service.ts index bf40c89..3e74d65 100644 --- a/src/app/core/services/server-directory.service.ts +++ b/src/app/core/services/server-directory.service.ts @@ -563,10 +563,10 @@ export class ServerDirectoryService { ); } - /** Notify the directory that a user has left a server. */ - notifyLeave(serverId: string, userId: string): Observable { + /** Remove a user's remembered membership after leaving a server. */ + notifyLeave(serverId: string, userId: string, selector?: ServerSourceSelector): Observable { return this.http - .post(`${this.buildApiBaseUrl()}/servers/${serverId}/leave`, { userId }) + .post(`${this.buildApiBaseUrl(selector)}/servers/${serverId}/leave`, { userId }) .pipe( catchError((error) => { console.error('Failed to notify leave:', error); diff --git a/src/app/features/invite/invite.component.html b/src/app/features/invite/invite.component.html index d75536e..5196c64 100644 --- a/src/app/features/invite/invite.component.html +++ b/src/app/features/invite/invite.component.html @@ -2,7 +2,9 @@
-
+
Invite link

@@ -51,9 +53,7 @@ @if (invite()!.server.hasPassword) { Password bypassed by invite } - - Expires {{ invite()!.expiresAt | date:'medium' }} - + Expires {{ invite()!.expiresAt | date: 'medium' }}

} diff --git a/src/app/features/server-search/server-search.component.html b/src/app/features/server-search/server-search.component.html index 3c01b5b..67bd6ce 100644 --- a/src/app/features/server-search/server-search.component.html +++ b/src/app/features/server-search/server-search.component.html @@ -165,9 +165,7 @@ Owner: {{ server.ownerName || server.ownerId || 'Unknown' }}
@if (server.hasPassword && !server.isPrivate && !isServerMarkedBanned(server)) { -
- Access: Password required -
+
Access: Password required
}
@@ -327,9 +325,7 @@ id="create-server-password" class="w-full px-3 py-2 bg-secondary rounded-lg border border-border text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-primary" /> -

- Users who already joined keep access even if you change the password later. -

+

Users who already joined keep access even if you change the password later.

diff --git a/src/app/features/servers/servers-rail.component.ts b/src/app/features/servers/servers-rail.component.ts index a58c462..cd1fcc3 100644 --- a/src/app/features/servers/servers-rail.component.ts +++ b/src/app/features/servers/servers-rail.component.ts @@ -123,6 +123,7 @@ export class ServersRailComponent { this.prepareVoiceContext(room); if (this.webrtc.hasJoinedServer(room.id) && roomWsUrl === currentWsUrl) { + this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: false })); this.store.dispatch(RoomsActions.viewServer({ room })); } else { await this.attemptJoinRoom(room); @@ -301,10 +302,31 @@ export class ServersRailComponent { if (errorCode === 'BANNED') { this.bannedServerName.set(room.name); this.showBannedDialog.set(true); + return; + } + + if (this.shouldFallbackToOfflineView(error)) { + this.closePasswordDialog(); + this.store.dispatch(RoomsActions.setSignalServerReconnecting({ isReconnecting: true })); + this.store.dispatch(RoomsActions.viewServer({ room })); } } } + private shouldFallbackToOfflineView(error: unknown): boolean { + const serverError = error as { + error?: { errorCode?: string }; + status?: number; + }; + const errorCode = serverError?.error?.errorCode; + const status = serverError?.status; + + return errorCode === 'SERVER_NOT_FOUND' + || status === 0 + || status === 404 + || (typeof status === 'number' && status >= 500); + } + private toServerInfo(room: Room) { return { id: room.id, diff --git a/src/app/features/shell/title-bar.component.html b/src/app/features/shell/title-bar.component.html index 2a6eb79..5910263 100644 --- a/src/app/features/shell/title-bar.component.html +++ b/src/app/features/shell/title-bar.component.html @@ -13,6 +13,16 @@ /> {{ roomName() }} + @if (showRoomReconnectNotice()) { + + + Reconnecting to signal server… + + } + @if (roomDescription()) {